# ====================================================================
# 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
