First pass at adding key files
This commit is contained in:
		@@ -0,0 +1,317 @@
 | 
			
		||||
# ====================================================================
 | 
			
		||||
# Gossamer Threads Module Library - http://gossamer-threads.com/
 | 
			
		||||
#
 | 
			
		||||
#   GT::Payment::Remote::2CheckOut
 | 
			
		||||
#   Author: Jason Rhinelander
 | 
			
		||||
#   CVS Info : 087,071,086,086,085      
 | 
			
		||||
#   $Id: 2CheckOut.pm,v 1.5 2006/08/22 20:39:04 brewt Exp $
 | 
			
		||||
#
 | 
			
		||||
# Copyright (c) 2004 Gossamer Threads Inc.  All Rights Reserved.
 | 
			
		||||
# ====================================================================
 | 
			
		||||
#
 | 
			
		||||
# Description:
 | 
			
		||||
#   2CheckOut payment processing.
 | 
			
		||||
#
 | 
			
		||||
 | 
			
		||||
package GT::Payment::Remote::2CheckOut;
 | 
			
		||||
use strict;
 | 
			
		||||
use Carp;
 | 
			
		||||
use GT::MD5 'md5_hex';
 | 
			
		||||
require Exporter;
 | 
			
		||||
use vars qw/@EXPORT_OK/;
 | 
			
		||||
@EXPORT_OK = qw/process/;
 | 
			
		||||
 | 
			
		||||
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};
 | 
			
		||||
 | 
			
		||||
    ref $opts{on_valid} eq 'CODE'
 | 
			
		||||
        or croak 'Usage: ->process(on_valid => \&CODEREF, ...)';
 | 
			
		||||
 | 
			
		||||
    defined $opts{password} and length $opts{password} or croak 'Usage: ->process(password => "password", ...)';
 | 
			
		||||
    defined $opts{sellerid} and length $opts{sellerid} or croak 'Usage: ->process(sellerid => "sellerid", ...)';
 | 
			
		||||
 | 
			
		||||
    $opts{password} eq 'tango' and croak 'Usage: ->process(password => "something other than \'tango\'", ...)';
 | 
			
		||||
 | 
			
		||||
    my $order_number = $in->param('order_number');
 | 
			
		||||
 | 
			
		||||
# Check that the "secret word" (password) combined with the other information
 | 
			
		||||
# actually checks out.
 | 
			
		||||
    my $str = $opts{password} . $opts{sellerid} . $order_number . $in->param('total');
 | 
			
		||||
    my $md5 = md5_hex($str);
 | 
			
		||||
 | 
			
		||||
    if (lc $md5 eq lc $in->param('key')) {
 | 
			
		||||
        $opts{on_valid}->();
 | 
			
		||||
    }
 | 
			
		||||
# If demo mode is enabled, then the order number is set to 1 in the md5:
 | 
			
		||||
# https://www.2checkout.com/documentation/UsersGuide2/chapter6/md5-hash.html
 | 
			
		||||
    elsif ($opts{demo}) {
 | 
			
		||||
        $str = $opts{password} . $opts{sellerid} . 1 . $in->param('total');
 | 
			
		||||
        $md5 = md5_hex($str);
 | 
			
		||||
 | 
			
		||||
        if (lc $md5 eq lc $in->param('key')) {
 | 
			
		||||
            $opts{on_valid}->();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
1;
 | 
			
		||||
 | 
			
		||||
__END__
 | 
			
		||||
 | 
			
		||||
=head1 NAME
 | 
			
		||||
 | 
			
		||||
GT::Payment::Remote::2CheckOut - 2CheckOut payment handling
 | 
			
		||||
 | 
			
		||||
=head1 CAVEATS
 | 
			
		||||
 | 
			
		||||
2CheckOut has a pretty weak automated payment system - the security of the
 | 
			
		||||
entire automated payment process hinges on your "Secret Word" (Admin -> Account
 | 
			
		||||
Details -> Return -> Secret Word (near the bottom of the page)) - without it,
 | 
			
		||||
there is no security at all.  Another weakness in the system is that if your
 | 
			
		||||
server is not reachable for whatever reason, the payment information would be
 | 
			
		||||
lost.  Payment providers like 2CheckOut and WorldPay would do well to learn
 | 
			
		||||
from payment systems like that of PayPal - whatever can be said about other
 | 
			
		||||
aspects of PayPal, they do have one of the nicest payment systems around - both
 | 
			
		||||
from a developer and user's point of view.
 | 
			
		||||
 | 
			
		||||
Because of the security issue with not using the "Secret Word", this module
 | 
			
		||||
requires that the secret word be used, even if other 2CheckOut systems may not.
 | 
			
		||||
Additionally, the default secret word of "tango" is not allowed.
 | 
			
		||||
 | 
			
		||||
=head1 SYNOPSIS
 | 
			
		||||
 | 
			
		||||
    use GT::Payment::Remote::2CheckOut;
 | 
			
		||||
    use GT::CGI;
 | 
			
		||||
 | 
			
		||||
    my $in = new GT::CGI;
 | 
			
		||||
 | 
			
		||||
    GT::Payment::Remote::2CheckOut->process(
 | 
			
		||||
        param => $in,
 | 
			
		||||
 | 
			
		||||
        on_valid => \&valid,
 | 
			
		||||
 | 
			
		||||
        sellerid => "1234",
 | 
			
		||||
        password => "Some Good Secret Word"
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    sub valid {
 | 
			
		||||
        # Update database - the payment has been made successfully.
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
=head1 DESCRIPTION
 | 
			
		||||
 | 
			
		||||
This module is designed to handle 2CheckOut payment processing.
 | 
			
		||||
 | 
			
		||||
=head1 REQUIREMENTS
 | 
			
		||||
 | 
			
		||||
GT::CGI and GT::MD5.
 | 
			
		||||
 | 
			
		||||
=head1 FUNCTIONS
 | 
			
		||||
 | 
			
		||||
This module has only one function: process() does the work of actually
 | 
			
		||||
figuring out what to do with a postback.
 | 
			
		||||
 | 
			
		||||
=head2 process
 | 
			
		||||
 | 
			
		||||
process() is the only function 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.
 | 
			
		||||
 | 
			
		||||
process() should be called for 2CheckOut initiated postbacks.  This can be set
 | 
			
		||||
up in your main .cgi by looking for 2CheckOut-specific CGI parameters
 | 
			
		||||
('cart_order_id' is a good one to look for) or by making a seperate .cgi file
 | 
			
		||||
exclusively for handling 2CheckOut postbacks.
 | 
			
		||||
 | 
			
		||||
Additionally, it is strongly advised that database connection, authenticate,
 | 
			
		||||
etc.  be performed before calling process() to ensure that the payment is
 | 
			
		||||
recorded successfully.  2CheckOut will not attempt to repost the form data if
 | 
			
		||||
your script produces an error, and the error will be shown to the customer.
 | 
			
		||||
 | 
			
		||||
=over 4
 | 
			
		||||
 | 
			
		||||
=item param
 | 
			
		||||
 | 
			
		||||
param takes a GT::CGI object from which 2CheckOut postback variables are read.
 | 
			
		||||
 | 
			
		||||
=item 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.  The
 | 
			
		||||
C<cart_order_id> CGI variable will have whatever cart_order_id you provided.
 | 
			
		||||
 | 
			
		||||
=item sellerid
 | 
			
		||||
 | 
			
		||||
This should be passed to seller number.  This is needed, along with the
 | 
			
		||||
password field below, to verify that the posted payment is a genuine 2CheckOut
 | 
			
		||||
payment.
 | 
			
		||||
 | 
			
		||||
=item password
 | 
			
		||||
 | 
			
		||||
This is a "Secret Word" that the admin must set in the 2CheckOut admin area
 | 
			
		||||
(under Look & Feel -> Secret Word).  This field must be set in the admin, and
 | 
			
		||||
passed in here.  Note that the default value, "tango", is not allowed.  Without
 | 
			
		||||
this password, 2CheckOut postbacks should not be considered secure.
 | 
			
		||||
 | 
			
		||||
=item demo
 | 
			
		||||
 | 
			
		||||
Whether or not to initiate and accept demo transactions.
 | 
			
		||||
 | 
			
		||||
=back
 | 
			
		||||
 | 
			
		||||
=head1 INSTRUCTIONS
 | 
			
		||||
 | 
			
		||||
To implement 2CheckOut payment processing, there are a number of steps required
 | 
			
		||||
in addition to this module.  Basically, this module handles only the postback
 | 
			
		||||
stage of the 2CheckOut payment process.
 | 
			
		||||
 | 
			
		||||
=head2 Directing customers to 2CheckOut
 | 
			
		||||
 | 
			
		||||
This is done by creating a web form containing the following variables.  Your
 | 
			
		||||
form, first of all, should post to
 | 
			
		||||
C<https://www.2checkout.com/2co/buyer/purchase>.  See
 | 
			
		||||
C<https://www.2checkout.com/documentation/UsersGuide2/third_party_carts/2co-system-parameters.html>
 | 
			
		||||
for a complete and up-to-date list of parameters that can be passed to 2CheckOut.
 | 
			
		||||
 | 
			
		||||
Required fields are as follows:
 | 
			
		||||
 | 
			
		||||
=over 4
 | 
			
		||||
 | 
			
		||||
=item * sid
 | 
			
		||||
    
 | 
			
		||||
Your 2CheckOut account number
 | 
			
		||||
 | 
			
		||||
=item * total
 | 
			
		||||
 | 
			
		||||
The total amount to be billed, in DD.CC format.
 | 
			
		||||
 | 
			
		||||
=item * cart_order_id
 | 
			
		||||
 | 
			
		||||
A unique order id, which you should store to track the payment.
 | 
			
		||||
 | 
			
		||||
=back
 | 
			
		||||
 | 
			
		||||
The following parameters *may* be passed in, and will be available in the
 | 
			
		||||
postback:
 | 
			
		||||
 | 
			
		||||
=over 4
 | 
			
		||||
 | 
			
		||||
=item * card_holder_name
 | 
			
		||||
 | 
			
		||||
=item * street_address
 | 
			
		||||
 | 
			
		||||
=item * city
 | 
			
		||||
 | 
			
		||||
=item * state
 | 
			
		||||
 | 
			
		||||
=item * zip
 | 
			
		||||
 | 
			
		||||
=item * country
 | 
			
		||||
 | 
			
		||||
=item * phone
 | 
			
		||||
 | 
			
		||||
The card holder's details.
 | 
			
		||||
 | 
			
		||||
=item * email
 | 
			
		||||
 | 
			
		||||
The card holder's email address.
 | 
			
		||||
 | 
			
		||||
=item * ship_name
 | 
			
		||||
 | 
			
		||||
=item * ship_street_address
 | 
			
		||||
 | 
			
		||||
=item * ship_city
 | 
			
		||||
 | 
			
		||||
=item * ship_state
 | 
			
		||||
 | 
			
		||||
=item * ship_zip
 | 
			
		||||
 | 
			
		||||
=item * ship_country
 | 
			
		||||
 | 
			
		||||
Shipping info - however, according to 2CheckOut, you must indicate that you
 | 
			
		||||
want to take that you want to take down a seperate shipping and billing address
 | 
			
		||||
on the L<Shipping Details page|https://sellers.2checkout.com/cgi-bin/sellersarea/shipdetails.2c>.
 | 
			
		||||
 | 
			
		||||
=item * demo
 | 
			
		||||
 | 
			
		||||
Should be set to 'Y' if you want demo mode, omitted for regular transactions.
 | 
			
		||||
 | 
			
		||||
=back
 | 
			
		||||
 | 
			
		||||
In the postback CGI, you'll get back all of the billing and shipping variables
 | 
			
		||||
listed above, plus:
 | 
			
		||||
 | 
			
		||||
=over 4
 | 
			
		||||
 | 
			
		||||
=item * order_number
 | 
			
		||||
 | 
			
		||||
2CheckOut order number
 | 
			
		||||
 | 
			
		||||
=item * cart_order_id
 | 
			
		||||
 | 
			
		||||
=item * cart_id
 | 
			
		||||
 | 
			
		||||
Your order number, passed back.  Both variables are the same.
 | 
			
		||||
 | 
			
		||||
=back
 | 
			
		||||
 | 
			
		||||
=head2 Postback
 | 
			
		||||
 | 
			
		||||
Before 2CheckOut postback notification can occur, you must set up the postback
 | 
			
		||||
(in 2CheckOut terminology, "Routine").  This can be set from the Admin ->
 | 
			
		||||
Shopping Cart -> Cart Details.  You need to enable the payment routine, and
 | 
			
		||||
set it to a CGI that you manage.
 | 
			
		||||
 | 
			
		||||
=head2 Putting it all together
 | 
			
		||||
 | 
			
		||||
The typical way to implement all of this is as follows:
 | 
			
		||||
 | 
			
		||||
=over 4
 | 
			
		||||
 | 
			
		||||
=item 1 Get necessary merchant information (sid and secret keyword)
 | 
			
		||||
 | 
			
		||||
=item 2 Once the customer has selected what to purchase, generate a
 | 
			
		||||
cart_order_id (a random MD5 hex string works well), and store it somewhere
 | 
			
		||||
(i.e. in the database).
 | 
			
		||||
 | 
			
		||||
=item 3 Make a form with all the necessary fields that
 | 
			
		||||
L<submits to 2CheckOut|/"Directing customers to 2CheckOut">.
 | 
			
		||||
 | 
			
		||||
=item 4 Set up the L<C<on_valid>|/"on_valid"> callback.  If using a dedicated
 | 
			
		||||
CGI script for 2CheckOut callbacks, it should just call process(); otherwise,
 | 
			
		||||
check for the CGI parameter 'cart_order_id' and if present, call process().
 | 
			
		||||
 | 
			
		||||
=item 5 For a valid payment, do whatever you need to do for a valid payment,
 | 
			
		||||
and store some record of the payment having been made (storing at least the
 | 
			
		||||
cart_order_id and the order_number is strongly recommended).  Use the CGI
 | 
			
		||||
parameter 'cart_order_id' to locate the order (i.e. in the database).
 | 
			
		||||
 | 
			
		||||
=back
 | 
			
		||||
 | 
			
		||||
=head1 SEE ALSO
 | 
			
		||||
 | 
			
		||||
L<http://www.2checkout.com> - 2CheckOut website.
 | 
			
		||||
 | 
			
		||||
L<http://www.support.2checkout.com/deskpro/faq.php> - 2CheckOut knowledgebase
 | 
			
		||||
 | 
			
		||||
=head1 MAINTAINER
 | 
			
		||||
 | 
			
		||||
Jason Rhinelander
 | 
			
		||||
 | 
			
		||||
=head1 COPYRIGHT
 | 
			
		||||
 | 
			
		||||
Copyright (c) 2004 Gossamer Threads Inc.  All Rights Reserved.
 | 
			
		||||
http://www.gossamer-threads.com/
 | 
			
		||||
 | 
			
		||||
=head1 VERSION
 | 
			
		||||
 | 
			
		||||
Revision: $Id: 2CheckOut.pm,v 1.5 2006/08/22 20:39:04 brewt Exp $
 | 
			
		||||
 | 
			
		||||
=cut
 | 
			
		||||
@@ -0,0 +1,573 @@
 | 
			
		||||
# ====================================================================
 | 
			
		||||
# 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
 | 
			
		||||
@@ -0,0 +1,466 @@
 | 
			
		||||
# ====================================================================
 | 
			
		||||
# Gossamer Threads Module Library - http://gossamer-threads.com/
 | 
			
		||||
#
 | 
			
		||||
#   GT::Payment::Remote::WorldPay
 | 
			
		||||
#   Author: Jason Rhinelander
 | 
			
		||||
#   CVS Info : 087,071,086,086,085      
 | 
			
		||||
#   $Id: WorldPay.pm,v 1.9 2006/08/22 23:03:14 brewt Exp $
 | 
			
		||||
#
 | 
			
		||||
# Copyright (c) 2004 Gossamer Threads Inc.  All Rights Reserved.
 | 
			
		||||
# ====================================================================
 | 
			
		||||
#
 | 
			
		||||
# Description:
 | 
			
		||||
#   WorldPay "Select Junior" payment processing.
 | 
			
		||||
#
 | 
			
		||||
#
 | 
			
		||||
# One major shortcoming of WorldPay is that its callback system is quite weak.
 | 
			
		||||
# It won't try to inform you very hard - it tries once, but if it doesn't
 | 
			
		||||
# connect it gives up and doesn't try again, making it entirely possible and
 | 
			
		||||
# likely that you will have to manually add missing payments at some point.
 | 
			
		||||
#
 | 
			
		||||
 | 
			
		||||
package GT::Payment::Remote::WorldPay;
 | 
			
		||||
use strict;
 | 
			
		||||
use Carp;
 | 
			
		||||
require Exporter;
 | 
			
		||||
use vars qw/@ISA @EXPORT_OK/;
 | 
			
		||||
@ISA = qw/Exporter/;
 | 
			
		||||
@EXPORT_OK = qw/process md5_signature/;
 | 
			
		||||
 | 
			
		||||
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};
 | 
			
		||||
 | 
			
		||||
    ref $opts{on_valid} eq 'CODE'
 | 
			
		||||
        or ref $opts{on_recurring} eq 'CODE'
 | 
			
		||||
        or croak 'Usage: ->process(on_valid => \&CODEREF, ...)';
 | 
			
		||||
 | 
			
		||||
    defined $opts{password} and length $opts{password} or croak 'Usage: ->process(password => "password", ...)';
 | 
			
		||||
 | 
			
		||||
    for (qw/on_valid on_recurring on_cancel on_invalid_password on_recurring_failed on_recurring_cancelled/) {
 | 
			
		||||
        !$opts{$_} or ref $opts{$_} eq 'CODE' or croak "Usage: ->process($_ => \\&CODEREF, ...)";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    my $callbackpw = $in->param('callbackPW');
 | 
			
		||||
    unless ($callbackpw and $callbackpw eq $opts{password}) {
 | 
			
		||||
        $opts{on_invalid_password}->() if $opts{on_invalid_password};
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    my $trans_status = $in->param('transStatus');
 | 
			
		||||
 | 
			
		||||
# The transaction was a testMode transaction, but testMode is not enabled.
 | 
			
		||||
    if ($in->param('testMode') and not $opts{test_mode}) {
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if ($in->param('futurePayId')) {
 | 
			
		||||
        if ($trans_status eq 'Y') {
 | 
			
		||||
            $opts{on_recurring}->() if $opts{on_recurring};
 | 
			
		||||
        }
 | 
			
		||||
        elsif ($trans_status eq 'N') {
 | 
			
		||||
            $opts{on_recurring_failed}->() if $opts{on_recurring_failed};
 | 
			
		||||
        }
 | 
			
		||||
        elsif ($in->param('futurePayStatusChange') eq 'Customer Cancelled') {
 | 
			
		||||
            $opts{on_recurring_cancelled}->() if $opts{on_recurring_cancelled};
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
        if    (uc $trans_status eq 'Y') { $opts{on_valid}->()  if $opts{on_valid}  }
 | 
			
		||||
        elsif (uc $trans_status eq 'C') { $opts{on_cancel}->() if $opts{on_cancel} }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
sub md5_signature {
 | 
			
		||||
# -----------------------------------------------------------------------------
 | 
			
		||||
    shift if $_[0] and UNIVERSAL::isa($_[0], __PACKAGE__);
 | 
			
		||||
    require GT::MD5;
 | 
			
		||||
    return GT::MD5::md5_hex(join ":", @_);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
1;
 | 
			
		||||
 | 
			
		||||
__END__
 | 
			
		||||
 | 
			
		||||
=head1 NAME
 | 
			
		||||
 | 
			
		||||
GT::Payment::Remote::WorldPay - WorldPay payment handling
 | 
			
		||||
 | 
			
		||||
=head1 CAVEATS
 | 
			
		||||
 | 
			
		||||
One thing to note about WorldPay is that its security system is a little weak -
 | 
			
		||||
you can't trust a callback post as actually being genuine, unless you use the
 | 
			
		||||
callback password feature - and even at that it is not a terribly secure
 | 
			
		||||
solution.  In this regard, other payment provides have much cleaner transaction
 | 
			
		||||
systems.  Another shortcoming of WorldPay is that its callback system is
 | 
			
		||||
somewhat weak - it won't try to inform you very hard: it tries once, but if it
 | 
			
		||||
doesn't connect it gives up and doesn't try again, making it entirely possible
 | 
			
		||||
and likely that you will have to manually add (or confirm) missing payments at
 | 
			
		||||
some point, so supporting at least manual payment approval of initiated
 | 
			
		||||
payments is absolutely required.
 | 
			
		||||
 | 
			
		||||
=head1 SYNOPSIS
 | 
			
		||||
 | 
			
		||||
    use GT::Payment::Remote::WorldPay;
 | 
			
		||||
    use GT::CGI;
 | 
			
		||||
 | 
			
		||||
    my $in = new GT::CGI;
 | 
			
		||||
 | 
			
		||||
    GT::Payment::Remote::WorldPay->process(
 | 
			
		||||
        param => $in,
 | 
			
		||||
        on_valid => \&valid,
 | 
			
		||||
        on_cancel => \&cancel,
 | 
			
		||||
 | 
			
		||||
        on_recurring => \&recurring,
 | 
			
		||||
        on_recurring_failed => \&recurring_failed,
 | 
			
		||||
        on_recurring_cancelled => \&recurring_cancelled,
 | 
			
		||||
 | 
			
		||||
        password => "123",
 | 
			
		||||
        on_invalid_password => \&invalid_pw
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    sub valid {
 | 
			
		||||
        # Update database - the payment has been made successfully.
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    sub cancel {
 | 
			
		||||
        # Update database - the user has clicked the "Cancel" button, thereby
 | 
			
		||||
        # cancelling the payment.  You should take note of the cancellation.
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    sub on_recurring {
 | 
			
		||||
        # Update database - a recurring payment has been made successfully.
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    sub on_recurring_failed {
 | 
			
		||||
        # Update database - a recurring payment has failed.
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    sub on_recurring_cancelled {
 | 
			
		||||
        # Update database - either the customer or the merchant has cancelled
 | 
			
		||||
        # this recurring payment
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    sub on_invalid_password {
 | 
			
		||||
        # Perhaps make a record - a payment callback was received without a
 | 
			
		||||
        # valid password
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
=head1 DESCRIPTION
 | 
			
		||||
 | 
			
		||||
This module is designed to handle WorldPay payment processing using WorldPay's
 | 
			
		||||
"Select Junior" system and callback.
 | 
			
		||||
 | 
			
		||||
=head1 REQUIREMENTS
 | 
			
		||||
 | 
			
		||||
GT::CGI is the only requirement, however GT::MD5 is required in order to use
 | 
			
		||||
the md5_signature function.
 | 
			
		||||
 | 
			
		||||
=head1 FUNCTIONS
 | 
			
		||||
 | 
			
		||||
This module has only two functions.  process() does the work of actually
 | 
			
		||||
figuring out what to do with a postback, and md5_signature() is used to
 | 
			
		||||
generate an MD5 signature for payment verification and security purposes.  Both
 | 
			
		||||
functions can be imported into your package, and can be called as either method
 | 
			
		||||
or function.
 | 
			
		||||
 | 
			
		||||
=head2 process
 | 
			
		||||
 | 
			
		||||
process() is the main function 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.
 | 
			
		||||
 | 
			
		||||
process() should be called for WorldPay initiated postbacks.  This can be set
 | 
			
		||||
up in your main CGI by looking for WorldPay-specific CGI parameters
 | 
			
		||||
('transStatus' is a good one to look for) or by making a seperate .cgi file
 | 
			
		||||
exclusively for handling WorldPay postbacks.
 | 
			
		||||
 | 
			
		||||
Additionally, it is strongly advised that database connection, authenticate,
 | 
			
		||||
etc.  be performed before calling process() to ensure that the payment is
 | 
			
		||||
recorded successfully.  WorldPay will not attempt to repost the form data if
 | 
			
		||||
your script produces an error, and the error will be shown to the customer.
 | 
			
		||||
 | 
			
		||||
The L<C<param>|/"param"> argument, either L<C<on_valid>|/"on_valid"> or
 | 
			
		||||
L<C<on_recurring>|/"on_recurring">, and the L<C<password>|/"password"> options
 | 
			
		||||
are required.  Using L<MD5 signing|/"MD5 signing"> as well is strongly advised.
 | 
			
		||||
 | 
			
		||||
=over 4
 | 
			
		||||
 | 
			
		||||
=item param
 | 
			
		||||
 | 
			
		||||
param takes a GT::CGI object from which WorldPay postback variables are read.
 | 
			
		||||
 | 
			
		||||
=item 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.
 | 
			
		||||
 | 
			
		||||
=item on_cancel
 | 
			
		||||
 | 
			
		||||
Takes a code reference to call in the event of the customer clicking the
 | 
			
		||||
"cancel" button.  Note that this is not sent if the user closes their browser,
 | 
			
		||||
but only if they click "cancel."
 | 
			
		||||
 | 
			
		||||
You should record a cancelled payment in your application.
 | 
			
		||||
 | 
			
		||||
=item password
 | 
			
		||||
 | 
			
		||||
This is a password that the customer should set in the WorldPay Customer
 | 
			
		||||
Management System, and provide to you.  Without this password, WorldPay
 | 
			
		||||
postbacks should not be considered secure.
 | 
			
		||||
 | 
			
		||||
=item on_invalid_password
 | 
			
		||||
 | 
			
		||||
This code reference will be called when the correct password is not present in
 | 
			
		||||
the postback request.  This will also be called if no password is provided.
 | 
			
		||||
 | 
			
		||||
=item on_recurring
 | 
			
		||||
 | 
			
		||||
=item on_recurring_failed
 | 
			
		||||
 | 
			
		||||
=item on_recurring_cancelled
 | 
			
		||||
 | 
			
		||||
In order to support recurring payments, you must at least define
 | 
			
		||||
C<on_recurring>.  C<on_recurring> is called when a successful recurring payment
 | 
			
		||||
has been made.  C<on_recurring_failed> is called for a failed recurring payment
 | 
			
		||||
(e.g. credit card declined).  See
 | 
			
		||||
L<the Recurring charges section|/"Recurring charges"> for more details.
 | 
			
		||||
 | 
			
		||||
Bear in mind that if you do not set up the on_recurring callback, recurring
 | 
			
		||||
payments will be ignored.
 | 
			
		||||
 | 
			
		||||
=back
 | 
			
		||||
 | 
			
		||||
=head2 md5_signature
 | 
			
		||||
 | 
			
		||||
The md5_signature() function takes a password (this must be set for the
 | 
			
		||||
WorldPay account), and a list of values and generates an appropriate WorldPay
 | 
			
		||||
MD5 signature, which should be included as the "signature" field.  See
 | 
			
		||||
L<the MD5 signing section|/"MD5 signing"> for more details.
 | 
			
		||||
 | 
			
		||||
=head1 INSTRUCTIONS
 | 
			
		||||
 | 
			
		||||
To implement WorldPay payment processing, there are a number of steps required
 | 
			
		||||
in addition to this module.  Basically, this module handles only the postback
 | 
			
		||||
stage of the WorldPay payment process.
 | 
			
		||||
 | 
			
		||||
Full WorldPay "Select Junior" information is available from the "Select Junior
 | 
			
		||||
Integration Guide" available from www.worldpay.com.
 | 
			
		||||
 | 
			
		||||
=head2 Directing customers to WorldPay
 | 
			
		||||
 | 
			
		||||
This is done by creating a web form containing the following variables.  Your
 | 
			
		||||
form, first of all, must make a C<post> request to
 | 
			
		||||
C<https://select.worldpay.com/wcc/purchase>.
 | 
			
		||||
 | 
			
		||||
Required fields are as follows:
 | 
			
		||||
 | 
			
		||||
=over 4
 | 
			
		||||
 | 
			
		||||
=item instId
 | 
			
		||||
 | 
			
		||||
Your WorldPay Installation ID.  Example: C<1234>
 | 
			
		||||
 | 
			
		||||
=item currency
 | 
			
		||||
 | 
			
		||||
The currency of the purchase.  Example: C<GBP>
 | 
			
		||||
 | 
			
		||||
=item desc
 | 
			
		||||
 | 
			
		||||
A description of the purchase.  Example: C<Blue T-Shirt, Medium>
 | 
			
		||||
 | 
			
		||||
=item cartId
 | 
			
		||||
 | 
			
		||||
A reference you assign to help you identify the purchase.  Example: C<10a0491>.
 | 
			
		||||
 | 
			
		||||
=item amount
 | 
			
		||||
 | 
			
		||||
The total cost of the purchase.  Example: C<25.35>
 | 
			
		||||
 | 
			
		||||
=back
 | 
			
		||||
 | 
			
		||||
=head2 Recurring charges
 | 
			
		||||
 | 
			
		||||
Additionally, in order to set up recurring payments, the WorldPay account must
 | 
			
		||||
have "FuturePay" enabled, and then you need to use the following parameters.
 | 
			
		||||
 | 
			
		||||
The below parameters are used for the "Regular FuturePay Agreements" - there is
 | 
			
		||||
also "Limited FuturePay Agreements" in which a maximum overall charge is set.
 | 
			
		||||
For more information, see L<Repear Billing With FuturePay|/"SEE ALSO">.
 | 
			
		||||
 | 
			
		||||
=over 4
 | 
			
		||||
 | 
			
		||||
=item futurePayType
 | 
			
		||||
 | 
			
		||||
Should contain the value "regular", unless using "Limited FuturePay Agreements,"
 | 
			
		||||
which will work but is not described here.
 | 
			
		||||
 | 
			
		||||
=item option
 | 
			
		||||
 | 
			
		||||
Should contain either 0, 1, or 2.  0 means the payment amount is fixed and
 | 
			
		||||
cannot be changed.  1 means the payment is fixed, but can be changed to another
 | 
			
		||||
amount at any point.  2 means the payment amount must be set before each
 | 
			
		||||
recurring payment.
 | 
			
		||||
 | 
			
		||||
=item startDate
 | 
			
		||||
 | 
			
		||||
Value in the format: "yyyy-mm-dd".  This should be the date on which the first
 | 
			
		||||
future payment should be taken.  Note that this is _NOT_ and CANNOT be today,
 | 
			
		||||
but must be a value in the future.  If using option 2, this value must be at
 | 
			
		||||
least 2 weeks in the future.
 | 
			
		||||
 | 
			
		||||
=item startDelayUnit
 | 
			
		||||
 | 
			
		||||
One digit: 1: day, 2: week, 3: month, 4: year.  Only used if startDate is
 | 
			
		||||
B<not> set.  If using option 2, this value must be at least 2 weeks in the
 | 
			
		||||
future.
 | 
			
		||||
 | 
			
		||||
=item startDelayMult
 | 
			
		||||
 | 
			
		||||
The actual delay is obtained by multiplying this value by startDelayUnit.  So,
 | 
			
		||||
to start in three weeks, this would be "3", and startDelayUnit would be "2".
 | 
			
		||||
Again, this is not used if startDate is specified.  Must be >= 1 if set.
 | 
			
		||||
 | 
			
		||||
=item noOfPayments
 | 
			
		||||
 | 
			
		||||
This number of payments that will be made.  Leave as 0 or unset for unlimited.
 | 
			
		||||
 | 
			
		||||
=item intervalUnit
 | 
			
		||||
 | 
			
		||||
One digit: 1: day, 2: week, 3: month, 4: year.  The unit of interval between
 | 
			
		||||
payments.  This must be set unless noOfPayments is 1.  If using option 1 or
 | 
			
		||||
option 2, the minimum interval is 2 weeks.
 | 
			
		||||
 | 
			
		||||
=item intervalMult
 | 
			
		||||
 | 
			
		||||
The interval between payments is determined by this value multiplied by
 | 
			
		||||
intervalUnit.  So, to make payments every 1 month, this would be "1", and
 | 
			
		||||
intervalUnit would be "3".  Must be >= 1.
 | 
			
		||||
 | 
			
		||||
=item normalAmount
 | 
			
		||||
 | 
			
		||||
This must be set for option 0 and option 1, but cannot be set for option 2.
 | 
			
		||||
 | 
			
		||||
=item initialAmount
 | 
			
		||||
 | 
			
		||||
This can be used for option 0 or option 1, but cannot be set for option 2.  If
 | 
			
		||||
set, this overrides the amount of the first payment.
 | 
			
		||||
 | 
			
		||||
=back
 | 
			
		||||
 | 
			
		||||
For FuturePay (recurring) payments, you still pass the required fields as
 | 
			
		||||
normal, except for the amount field: amount can be passed as 0 or a value - if
 | 
			
		||||
a value is specified, this will be treated as an immediate payment.  So, for
 | 
			
		||||
example, if you wanted to charge someone a monthly subscription of $10 starting
 | 
			
		||||
today you would pass the following variables:
 | 
			
		||||
 | 
			
		||||
    instId=1234 # (the merchant's installation reference here)
 | 
			
		||||
    amount=10
 | 
			
		||||
    cartId=8456a9264q314 # (Some random ID here that you generate)
 | 
			
		||||
    currency=USD # (Whatever currency they are charging in goes here)
 | 
			
		||||
    desc=Subscription For Something Cool # (Description of subscription)
 | 
			
		||||
    option=0
 | 
			
		||||
    normalAmount=10
 | 
			
		||||
    startDelayUnit=3
 | 
			
		||||
    startDelayMult=1
 | 
			
		||||
    intervalUnit=3
 | 
			
		||||
    intervalMult=1
 | 
			
		||||
 | 
			
		||||
=head2 MD5 signing
 | 
			
		||||
 | 
			
		||||
Additionally, using WorldPay's MD5 signature feature is strongly recommended.
 | 
			
		||||
 | 
			
		||||
To enable this feature, provide a field "signatureFields", containing fields
 | 
			
		||||
separated by ":".  Although any fields can be used, "amount:currency:cartId" is
 | 
			
		||||
recommended.  Then, call:
 | 
			
		||||
 | 
			
		||||
    my $md5 = GT::Payment::Remote::WorldPay::md5_signature(
 | 
			
		||||
        $password, $amount, $currency, $cartId
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
$password should be a password provided by the user and known only to the user
 | 
			
		||||
and WorldPay.  The value returned should be passed as the "signature" variable.
 | 
			
		||||
 | 
			
		||||
This MD5 protection causes WorldPay to reject any faked payment requests and so
 | 
			
		||||
is reasonably secure.
 | 
			
		||||
 | 
			
		||||
=head2 Postback
 | 
			
		||||
 | 
			
		||||
Before WorldPay postback notification can occur, you must instruct the user to
 | 
			
		||||
enable the callback facility in the Customer Management System.  Additionally,
 | 
			
		||||
it is recommended that a proper URL to your CGI be specified there, or else
 | 
			
		||||
pass along a "MC_callback" variable that points to the script _WITHOUT_ a
 | 
			
		||||
leading http:// or https://. (e.g. MC_callback=www.example.com/callback.cgi).
 | 
			
		||||
 | 
			
		||||
Note that a WorldPay limitation prevents the callback protocol (http://) from
 | 
			
		||||
being changed dynamically - whatever protocol is set for your callback URL in
 | 
			
		||||
the Customer Management System will be used with the dynamic callback URL.
 | 
			
		||||
 | 
			
		||||
=head2 Putting it all together
 | 
			
		||||
 | 
			
		||||
The typical way to implement all of this is as follows:
 | 
			
		||||
 | 
			
		||||
=over 4
 | 
			
		||||
 | 
			
		||||
=item 1 Get necessary merchant information (instId, currency, callback
 | 
			
		||||
password, and MD5 password).
 | 
			
		||||
 | 
			
		||||
=item 2 Once the customer has selected what to purchase, generate a cartId (a
 | 
			
		||||
random MD5 hex string works well - but I<do not> use the MD5 signature!), and
 | 
			
		||||
L<generate the MD5 signature|/"MD5 signing">.
 | 
			
		||||
 | 
			
		||||
=item 3 Store the cartId somewhere (i.e. in the database).
 | 
			
		||||
 | 
			
		||||
=item 4 Make a form with all the necessary fields that
 | 
			
		||||
L<submits to WorldPay|/"Directing customers to WorldPay">.
 | 
			
		||||
 | 
			
		||||
=item 5 Set up the necessary callbacks (at least L<C<on_valid>|/"on_valid"> and
 | 
			
		||||
L<C<on_valid>|/"on_cancel">).  If using a dedicated CGI script for WorldPay
 | 
			
		||||
callbacks, it should just call process(); otherwise, check for the CGI
 | 
			
		||||
parameter 'transStatus' and if present, call process().
 | 
			
		||||
 | 
			
		||||
=item 6 For a valid payment, do whatever you need to do for a valid payment,
 | 
			
		||||
and store some record of the payment having been made (storing at least the
 | 
			
		||||
cartId, the transId, and the futurePayId is strongly recommended).  Use the CGI
 | 
			
		||||
parameter 'cartId' to locate the order (i.e. in the database).  It's
 | 
			
		||||
recommended that you check Appendix A of the "Select Junior Integration Guide"
 | 
			
		||||
for all available parameters.
 | 
			
		||||
 | 
			
		||||
=back
 | 
			
		||||
 | 
			
		||||
=head1 SEE ALSO
 | 
			
		||||
 | 
			
		||||
L<http://support.worldpay.com> - WorldPay Knowledge Base, containing many
 | 
			
		||||
useful WorldPay manuals and instructions.
 | 
			
		||||
 | 
			
		||||
L<http://support.worldpay.com/kb/integration_guides/junior/integration/help/sjig.html>
 | 
			
		||||
- Select Junior Integration Guide, from which this documentation and module is
 | 
			
		||||
primarily derived.
 | 
			
		||||
 | 
			
		||||
L<http://support.worldpay.com/kb/product_guides/futurepay/repeatbilling.html> -
 | 
			
		||||
Repeat Billing with FuturePay.
 | 
			
		||||
 | 
			
		||||
=head1 MAINTAINER
 | 
			
		||||
 | 
			
		||||
Jason Rhinelander
 | 
			
		||||
 | 
			
		||||
=head1 COPYRIGHT
 | 
			
		||||
 | 
			
		||||
Copyright (c) 2004 Gossamer Threads Inc.  All Rights Reserved.
 | 
			
		||||
http://www.gossamer-threads.com/
 | 
			
		||||
 | 
			
		||||
=head1 VERSION
 | 
			
		||||
 | 
			
		||||
Revision: $Id: WorldPay.pm,v 1.9 2006/08/22 23:03:14 brewt Exp $
 | 
			
		||||
 | 
			
		||||
This module is designed for version 4.4 of the Select Junior payment
 | 
			
		||||
integration.
 | 
			
		||||
 | 
			
		||||
=cut
 | 
			
		||||
		Reference in New Issue
	
	Block a user