# ==================================================================== # 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 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 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 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 the same, you should return C 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 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. Your form should contains various PayPal parameters, as outlined in the PayPal manuals linked to in the L 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 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 - Single Item Purchases Manual L - Subscriptions and Recurring Payments Manual L - 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