</div>
</div>
-<form action="[% ctx.opac_root %]/biblio/main_payment_form" method="GET" style="background:#fff">
+[%
+ pay_form_url = ctx.opac_root _ '/biblio/main_payment_form';
+ IF ctx.using_payflow OR CGI.param('use_payflow');
+ pay_form_url = ctx.opac_root _ '/payflow/pay_form';
+ END;
+%]
+<form action="[% pay_form_url %]" method="GET" style="background:#fff">
[% IF ctx.fines.circulation.size > 0 %]
<div id='myopac_circ_trans_div'>
<table width='100%' class='data_grid'>
--- /dev/null
+<div>
+ <br/>
+ <p>[% l('Selected fines you are paying for:') %]</p>
+
+ <table cellpadding="0" cellspacing="0" border="0" class="myopac_payments_table">
+ <thead>
+ <tr><th>[% l('Name') %]</th><th>[% l('Amount') %]</th></tr>
+ </thead>
+
+ <tbody>
+ [%
+ FOR f IN ctx.fines.circulation;
+ NEXT IF CGI.param('xact').size &&
+ !CGI.param('xact').grep(f.xact.id).size;
+ attrs = {marc_xml => f.marc_xml};
+ IF f.marc_xml;
+ PROCESS get_marc_attrs args=attrs;
+ ELSIF f.xact.reservation;
+ attrs.title = f.xact.reservation.target_resource_type.name;
+ END %]
+ <tr>
+ <td>[% attrs.title | html %]</td>
+ <td class="text-right">[% money(f.xact.balance_owed) %]</td>
+ </tr>
+ [%
+ END;
+ FOR f IN ctx.fines.grocery;
+ NEXT IF CGI.param('xact_misc').size &&
+ !CGI.param('xact_misc').grep(f.xact.id).size %]
+ <tr>
+ <td>[% f.xact.last_billing_type | html %]</td>
+ <td class="text-right">[% money(f.xact.balance_owed) %]</td>
+ </tr>
+ [% END %]
+ </tbody>
+ </table>
+
+ <br/>
+
+ <div>
+ [% l('Total amount to pay:') %]
+ <strong>[% money(ctx.fines.balance_owed) %]</strong>
+ </div>
+
+</div>
+
--- /dev/null
+<div class="payment-error">
+[%
+
+# Map PayFlow POST response codes to patron messages.
+# https://developer.paypal.com/docs/classic/payflow/integration-guide/#result-values-and-respmsg-text
+
+SWITCH ctx.payflow_hosted_ctx.RESULT;
+
+ CASE '12'; # Declined.
+ l('Declined. Please verify your card details.');
+
+ CASE '23'; # Invalid CC number (e..g mis-typed)
+ l('Declined. Please make sure you entered your credit card number correctly.');
+
+ CASE '25'; # Transaction type not mapped to this host
+ l('KCLS does not accept AMEX or Discover Card at this time. Please use your Visa or MasterCard.');
+
+ CASE '114'; # CVV2 or CID Mismatch
+ l('Declined. Please make sure you entered the three digit code on the back of your card correctly.');
+
+ CASE '125';
+ handled = 0;
+ l('Declined. ');
+
+ IF ctx.payflow_hosted_ctx.PROCCVV2 == 'N';
+ l('Please make sure you entered the three digit code on the back of your card correctly. ');
+ handled = 1;
+ END;
+
+ IF NOT handled
+ OR ctx.payflow_hosted_ctx.AVSADDR == 'N'
+ OR ctx.payflow_hosted_ctx.AVSZIP == 'N';
+ l('Please make sure your billing information matches what the bank has on file for your credit card.');
+ END;
+
+ CASE DEFAULT;
+ l('An unkown error occurred attempting credit card payment.');
+END;
+
+%]
+</div>
--- /dev/null
+<div id="footer">
+ <div class="float-left"</div>
+ <a href="http://www.kcls.org/ask/"
+ style="font-size: 14px; color: white; font-family: Arial, Helvetica, sans-serif; font-weight: 600;" >
+ [% l('Ask KCLS') %]
+ </a>
+ <div class="common-no-pad"></div>
+</div>
+
--- /dev/null
+
+<div id='cc-form-warning' class='payment-error' style='display:none'>
+ <strong>Please enter values for all required fields.</strong>
+ <br/>
+</div>
+
+<form method="POST" id='cc-form'>
+
+ [% FOR xact IN CGI.param('xact') %]
+ <input type="hidden" name="xact" value="[% xact | html %]" />
+ [% END %]
+
+ [% FOR xact IN CGI.param('xact_misc') %]
+ <input type="hidden" name="xact_misc" value="[% xact | html %]" />
+ [% END %]
+
+ <table>
+ <tbody>
+ <tr>
+ <td>[% l('First Name *') %]</td>
+ <td><input type="text" name="BILLTOFIRSTNAME"
+ value="[% ctx.payflow_hosted_ctx.payflow_params.BILLTOFIRSTNAME
+ || ctx.user.first_given_name | html %]" /></td>
+ </tr>
+ <tr>
+ <td>[% l('Last Name *') %]</td>
+ <td><input type="text" name="BILLTOLASTNAME"
+ value="[% ctx.payflow_hosted_ctx.payflow_params.BILLTOLASTNAME
+ || ctx.user.family_name | html %]" /></td>
+ </tr>
+ <tr>
+ <td>[% l('Email Address') %]</td>
+ <td>
+ <input type="text" name="BILLTOEMAIL"
+ value="[% ctx.payflow_hosted_ctx.payflow_params.BILLTOEMAIL
+ || ctx.user.email | html %]" />
+ </td>
+ </tr>
+ <tr>
+ <td colspan='3'><strong>
+ [% l('Please use the address that is on your bank/credit card statement.') %]
+ </strong></td>
+ </tr>
+ <tr>
+ <td>[% l('Street Address *') %]</td>
+ <td><input type="text" name="BILLTOSTREET"
+ value="[% ctx.payflow_hosted_ctx.payflow_params.BILLTOSTREET | html %]"/></td>
+ </tr>
+ <tr>
+ <td>[% l('City *')%]</td>
+ <td><input type="text" name="BILLTOCITY"
+ value="[% ctx.payflow_hosted_ctx.payflow_params.BILLTOCITY
+ || ctx.user.billing_address.city | html %]" /></td>
+ </tr>
+ <tr>
+ <td>[% l('State or Province *') %]</td>
+ <td><input type="text" name="BILLTOSTATE"
+ value="[% ctx.payflow_hosted_ctx.payflow_params.BILLTOSTATE
+ || ctx.user.billing_address.state | html %]" /></td>
+ </tr>
+ <tr>
+ <td>[% l('ZIP or Postal Code *') %]</td>
+ <td><input type="text" name="BILLTOZIP"
+ value="[% ctx.payflow_hosted_ctx.payflow_params.BILLTOZIP
+ || ctx.user.billing_address.post_code | html %]" /></td>
+ </tr>
+ <tr>
+ <td colspan='2' align="center">
+ <input type="submit" value="[% l('Next') %]"
+ onclick='return check_cc_params()'/>
+ <a href="[% mkurl(ctx.opac_root _ '/biblio/main_fines', {}, 1) %]">
+ [% l('Cancel') %]
+ </a>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+</form>
+
+<script>
+ //var post_regex = new RegExp(/^\d{5}(?:[-\s]\d{4})?$/);
+ var fields = ['ZIP', 'STATE', 'CITY', 'STREET', 'FIRSTNAME', 'LASTNAME'];
+
+ function check_cc_params() {
+ var form = document.getElementById('cc-form');
+ var msg = document.getElementById('cc-form-warning');
+ msg.style.display = 'none';
+
+ var valid = true;
+ for (var i = 0; i < fields.length; i++) {
+ var fdom = form['BILLTO' + fields[i]];
+ if (fdom.value) {
+ fdom.className = '';
+ } else {
+ fdom.className = 'cc-form-invalid-field';
+ valid = false;
+ msg.style.display = 'block';
+ }
+ }
+
+ return valid;
+ }
+</script>
+
+
--- /dev/null
+<table>
+ <tbody>
+ <tr>
+ <td>
+ <!-- paypal CC payment iframe -->
+
+ [% IF ctx.payflow_hosted_error %]
+ <strong>
+ An error occurred communicating with PayPal.
+ Unable to process payment at this time.
+ </strong>
+ [% STOP %]
+ [% END %]
+
+ [%
+ token = ctx.payflow_hosted_ctx.secure_token | uri;
+ token_id = ctx.payflow_hosted_ctx.secure_token_id | uri;
+ iframe_url = ctx.payflow_hosted_ctx.hosted_server _
+ '?SECURETOKEN=' _ token _ '&SECURETOKENID=' _ token_id;
+ IF ctx.payflow_hosted_ctx.test_mode;
+ iframe_url = iframe_url _ '&MODE=TEST';
+ END;
+ %]
+ <style>
+ #pp-iframe { width: 600px; height: 650px ; scroll: auto}
+ </style>
+ <iframe id='pp-iframe' src="[% iframe_url %]"> </iframe>
+ </td>
+ </tr>
+ </tbody>
+</table>
+
--- /dev/null
+[%
+ PROCESS "opac/parts/header.tt2";
+ PROCESS "opac/parts/misc_util.tt2";
+ WRAPPER "opac/biblio/base.tt2";
+ last_chance = CGI.param("last_chance");
+%]
+[% INCLUDE "opac/biblio/topnav.tt2" %]
+
+<div id='fines_payments_wrapper'>
+ <div id='acct_fines_tabs'>
+ <a href='[% ctx.opac_root %]/biblio/main_fines'>
+ <img src='[% ctx.media_prefix %]/images/acct_fines_off.jpg'/></a>
+ <a href='[% ctx.opac_root %]/biblio/main_payments'>
+ <img src='[% ctx.media_prefix %]/images/acct_payments_on.jpg'/></a>
+ </div>
+</div>
+
+<style>
+ #pay-form-container {
+ background: #fff;
+ width: 98%;
+ padding: 5px;
+ }
+ #pay-form-div {
+ float:left;
+ }
+ #pay-xacts-div {
+ float:left;
+ margin-left: 20px;
+ }
+ .cc-form-invalid-field {
+ color: black;
+ background-color: red;
+ }
+</style>
+
+<div id="pay-form-container">
+
+ <div>
+ <h2>[% l('KCLS only accepts Visa or MasterCard') %]</h2>
+ <br/>
+ <strong>[% l('Billing Information') %]</strong>
+ </div>
+ <div class="clear-both"></div>
+
+ <div id='pay-form-div'>
+
+ [% IF ctx.payflow_hosted_ctx.secure_token_id
+ AND NOT ctx.payflow_hosted_ctx.pay_result_code %]
+
+ <!-- We have a new secure token. Display the PayPal iframe -->
+ [% INCLUDE 'opac/payflow/form2.tt2' %]
+
+ [% ELSE %]
+
+ [% IF ctx.payflow_hosted_ctx.pay_result_code %]
+ <!-- Previous payment attempt was rejected by PP -->
+ [% INCLUDE 'opac/payflow/errors.tt2' %]
+ [% ELSIF ctx.payflow_hosted_ctx.init_error %]
+ <div class="payment-error">
+ [% l('Error initializing credit card payments. Unable to make payments at this time.') %]
+ </div>
+ [% END %]
+
+ <!-- before we talk to paypal, collect address, etc. info from patron -->
+ [% INCLUDE 'opac/payflow/form1.tt2' %]
+
+ [% END %]
+ </div>
+ <div id='pay-xacts-div'>
+ [% INCLUDE "opac/parts/myopac/payment_xacts.tt2" %]
+ <br />
+ <span>
+ Click
+ <strong>
+ <a href="[% mkurl(ctx.opac_root _ '/biblio/main_fines', {}, 1) %]">
+ [% l('Cancel') %]
+ </a>
+ </strong>
+ to go back and change your selection.
+ </span>
+ </div>
+ <div class="clear-both"></div>
+
+ <br/>
+ <table>
+ [% INCLUDE "opac/biblio/main_refund_policy.tt2" %]
+ </table>
+
+</div>
+
+[% INCLUDE 'opac/payflow/footer.tt2' %]
+
+<script src='/js/ui/default/opac/kcls.js'></script>
+
+[% END %]
--- /dev/null
+[%
+ WRAPPER "opac/biblio/base.tt2";
+ PROCESS "opac/parts/header.tt2";
+%]
+
+[% INCLUDE "opac/biblio/topnav.tt2" %]
+
+<div id='fines_payments_wrapper'>
+ <div id='acct_fines_tabs'>
+ <a href='[% ctx.opac_root %]/biblio/main_fines'>
+ <img src='[% ctx.media_prefix %]/images/acct_fines_on.jpg'/></a>
+ <a href='[% ctx.opac_root %]/biblio/main_payments'>
+ <img src='[% ctx.media_prefix %]/images/acct_payments_off.jpg'/></a>
+ </div>
+</div>
+
+<div id="myopac_summary_div" style="background-color:#FFF;">
+
+ <p><big>[% l('Your payment has been approved.') %]</big></p>
+
+ [% IF ctx.printable_receipt.template_output;
+ print_args = [];
+ FOR p IN ctx.payflow_hosted_ctx.payments;
+ print_args.push('payment=' _ p);
+ END %]
+
+ <p>[
+ <a href="[% ctx.opac_root %]/biblio/receipt_print?[% print_args.join('&') %]"
+ target="_egrecpt"
+ onclick="try { print_node('printable-receipt'); } catch (e) { window.print(); } return false;">[% l('Print receipt') %]</a>
+ ]
+ </p>
+ <tt id="printable-receipt">
+ [% ctx.printable_receipt.template_output.data %]
+ </tt>
+ [% ELSE %]
+ <div class="payment-error">
+ [% l(
+ 'Error creating receipt: [_1]',
+ ( ctx.printable_receipt.textcode ?
+ ctx.printable_receipt.textcode _ ' / ' _
+ ctx.printable_receipt.desc : 0
+ ) ||
+ ctx.printable_receipt.error_output.data ||
+ l('No receipt data returned from server')
+ ) | html
+ %]
+ </div>
+ [% END %]
+
+ <p>[ <a href="[% ctx.opac_root %]/biblio/main_fines">[%
+ l("Back to Account Summary") %]</a> ]</p>
+
+</div>
+
+[% INCLUDE 'opac/payflow/footer.tt2' %]
+
+[% END %]
--- /dev/null
+<!--
+ This page is loaded within an iframe. Keep it as slim as possible.
+ No footers, etc.
+-->
+[% WRAPPER "opac/biblio/base.tt2" %]
+<div id="myopac_summary_div" style="background:#fff">
+
+ <script>
+ function go_to_url(url) {
+ // If we're inside an iframe, this redirects the parent frame
+ // to the requested URL. Otherwise, redirects the current page.
+ var win = window.top ? window.top : window;
+ win.location.href = url;
+ }
+ </script>
+
+[% IF ctx.payflow_hosted_ctx.pay_result_code
+ AND ctx.payflow_hosted_ctx.pay_result_code != '0' %]
+
+ <!--
+ Payment rejected.
+ Redirect the parent frame to the form1 page to display the error message.
+ -->
+ <script>
+ setTimeout(
+ function(){ go_to_url(
+ '[% ctx.opac_root _ "/payflow/pay_form/" _
+ ctx.payflow_hosted_ctx.secure_token_id %]') },
+ 100
+ );
+ </script>
+
+[% ELSIF ctx.on_processing_page %]
+
+ <div class="payment-processing">
+ [% l('Processing...') %] <br/><br/>
+ [% l("Please do not Refresh or use your browser's Back button") %]<br/>
+ </div>
+
+[% ELSIF ctx.payment_response.textcode %]
+ <!--
+ Payment tracking attempt failed. Display the error message
+ then offer a button to escape the iframe by redirecting the
+ parent frame to the main fines page.
+ -->
+
+ <div class="payment-error">
+ <span>
+ Error processing payment after credit card payment succeeded.
+ Please see staff to complete transaction.
+ <br/><br/>
+ Payment order number: <b>[% ctx.payflow_hosted_ctx.order_number %]</b>.
+ </span>
+
+ <span title="[% ctx.payment_response.textcode %]">
+ [% ctx.payment_response.desc || ctx.payment_response.textcode %]
+ </span>
+ <br/>
+ [% ctx.payment_response.note %]
+ [% ctx.payment_response.payload.error_message %]
+
+ </div>
+ <p>
+ [%
+ url_args = {xact => [], xact_misc => []};
+ FOR k IN ['xact', 'xact_misc'];
+ FOR val IN CGI.param(k);
+ url_args.$k.push(val);
+ END;
+ END;
+ retry_url = mkurl(ctx.opac_root _ '/payflow/pay_form', url_args, 1);
+ %]
+ <br/>
+ <a href="javascript:go_to_url('[% retry_url %]')">[% l('Go back') %]</a>
+ [% l('to try again or to cancel this payment attempt.') %]
+ </p>
+
+[% ELSE %]
+ <!--
+ Payment succeeded.
+ Redirect the parent frame to the receipts page.
+ -->
+ <script>
+ setTimeout(
+ function(){ go_to_url(
+ '[% ctx.opac_root _ "/payflow/pay_receipt/" _
+ ctx.payflow_hosted_ctx.secure_token_id %]') },
+ 100
+ );
+ </script>
+
+[% END %]
+
+</div>
+[% END %]
--- /dev/null
+-- Deploy kcls-evergreen:payflow-hosted-org-settings to pg
+-- requires: vand-auth-edit-date
+
+BEGIN;
+
+INSERT INTO config.org_unit_setting_type
+ (name, label, description, datatype, view_perm, update_perm, grp)
+VALUES
+ ('credit.processor.payflowhosted.partner', 'PayflowHosted Partner',
+ 'PayPal Partner value. Typically set to ''PayPal''',
+ 'string', 1005, 1006, 'credit'),
+ ('credit.processor.payflowhosted.vendor', 'PayflowHosted Vendor',
+ 'PayPal Vendor account name',
+ 'string', 1005, 1006, 'credit'),
+ ('credit.processor.payflowhosted.login', 'PayflowHosted User/Login',
+ 'PayPal login used for facilitating transactions',
+ 'string', 1005, 1006, 'credit'),
+ ('credit.processor.payflowhosted.password', 'PayflowHosted Password',
+ 'Password for PayPal login account',
+ 'string', 1005, 1006, 'credit'),
+ ('credit.processor.payflowhosted.testmode', 'PayflowHosted Test Mode',
+ 'Set to true if transactions should be sent to the PayPal test ' ||
+ 'server as test transactions. When activated, no credit cards ' ||
+ 'will be charged, but payments will be generated in Evergreen',
+ 'bool', 1005, 1006, 'credit'),
+ ('credit.processor.payflowhosted.enabled', 'PayflowHosted Enabled',
+ 'Set to true if this credit card processor should be used',
+ 'bool', 1005, 1006, 'credit'),
+ ('credit.processor.payflowhosted.autohosts',
+ 'PayflowHosted Dynamic Response Hosts',
+ 'Set to true if PayPal should send responses to the same server ' ||
+ 'where the credit card payment was initiated (using the hostname ' ||
+ 'reported by the web server). Otherwise, all responses are ' ||
+ 'delivered to the return/error/cancel URLs configured with PayPay',
+ 'bool', 1005, 1006, 'credit')
+
+;
+
+COMMIT;
--- /dev/null
+-- Revert kcls-evergreen:payflow-hosted-org-settings from pg
+
+BEGIN;
+
+DELETE FROM config.org_unit_setting_type
+ WHERE name LIKE 'credit.processor.payflowhosted.%';
+
+COMMIT;
+
connexion-auth-imports [sip-activity-types] 2016-05-11T15:10:49Z Bill Erickson,,, <berick@teapot> # OCLC Connexion Authority Record Imports Data
purge-user-activity [sip-activity-types] 2016-04-29T17:07:46Z Bill Erickson <berickxx@gmail.com> # Clean up actor.usr_activity
vand-auth-edit-date [purge-user-activity] 2016-06-01T18:24:54Z Bill Erickson <berickxx@gmail.com> # Vandelay authority import sets edit[or|_date]
+payflow-hosted-org-settings [vand-auth-edit-date] 2016-07-06T18:39:40Z Bill Erickson <berickxx@gmail.com> # PayflowPro Hosted Pages org unit settings
--- /dev/null
+-- Verify kcls-evergreen:payflow-hosted-org-settings on pg
+
+BEGIN;
+
+-- XXX Add verifications here.
+
+ROLLBACK;
my ($class, $e, $circ) = @_;
my $can_close = 0;
- my $reason = $circ->stop_fines;
+ my $reason = $circ->stop_fines || '';
# We definitely want to close if this circulation was
# checked in or renewed.
return OpenILS::Event->new(
'BAD_PARAMS', note => 'Need approval code'
) if not $cc_args->{approval_code};
+
+ # Out of band processors send completed payment info via cc_args.
+ $cc_processor = $cc_args->{processor};
+ $cc_order_number = $cc_args->{order_number};
}
}
return $self->load_myopac_payments if $path =~ m|opac/myopac/main_payments|;
return $self->load_myopac_pay_init if $path =~ m|opac/myopac/main_pay_init|;
return $self->load_myopac_pay if $path =~ m|opac/myopac/main_pay|;
+ return $self->load_myopac_pay_response if $path =~ m|opac/myopac/pay_response|;
return $self->load_myopac_main if $path =~ m|opac/myopac/main|;
return $self->load_myopac_receipt_email if $path =~ m|opac/myopac/receipt_email|;
return $self->load_myopac_receipt_print if $path =~ m|opac/myopac/receipt_print|;
return $self->load_myopac_prefs_my_lists if $path =~ m|opac/myopac/prefs_my_lists|;
return $self->load_myopac_prefs if $path =~ m|opac/myopac/prefs|;
return $self->load_sms_cn if $path =~ m|opac/sms_cn|;
-
+
+ # PayflowHosted E-Com pages.
+ return $self->load_myopac_payflow_form if $path =~ m|opac/payflow/pay_form|;
+ return $self->load_myopac_payflow_response if $path =~ m|opac/payflow/pay_response|;
+ return $self->load_myopac_payflow_receipt if $path =~ m|opac/payflow/pay_receipt|;
+
#BiblioCommons E-Commerce Screens
return $self->load_myopac_payment_form if $path =~ m|opac/biblio/main_payment_form|;
return $self->load_myopac_payments if $path =~ m|opac/biblio/main_payments|;
use OpenSRF::Utils::Cache;
use Digest::MD5 qw(md5_hex);
use Data::Dumper;
+use OpenILS::WWW::EGCatLoader::PayflowHosted;
$Data::Dumper::Indent = 0;
use DateTime;
use DateTime::Format::ISO8601;
my @user_prefs = qw/
opac.lists_per_page
- opac.lists_per_page,
+ opac.lists_per_page
/;
my $stat = $self->_load_user_with_prefs;
return Apache2::Const::OK;
}
+# UI for entering billing address, etc. data.
+sub load_myopac_payflow_form {
+ my $self = shift;
+ my $token_id = $self->ctx->{page_args}->[0];
+ my $tokens;
+
+ my $stat = $self->prepare_extended_user_info;
+ return $stat if $stat;
+
+ # If this is a new payment, the transactions will come from GET params.
+ my $xacts = [$self->cgi->param('xact'), $self->cgi->param('xact_misc')];
+
+ if ($token_id) {
+ # We were directed back to the form1 page after a payment attempt
+ # at PP was rejected with a non-zero status.
+ # Re-display form1 with error info.
+
+ my $cache = OpenSRF::Utils::Cache->new('global');
+ $tokens = $cache->get_cache($token_id);
+
+ if (!$tokens) {
+
+ $logger->error("PayflowHosted payment was rejected, but ".
+ "we were unable to retrieve the payment context data ".
+ "from the cache. Unable to continue payment!");
+
+ $self->ctx->{payflow_hosted_ctx} = {error => 1};
+ return Apache2::Const::OK;
+ }
+
+ # After we render this page, this payment context is no longer valid.
+ $self->ctx->{payflow_hosted_ctx} = $tokens;
+
+ # If this is an existing payment, pull the transactions from
+ # the payment context blob.
+ $xacts = $tokens->{xacts};
+ }
+
+ $stat = $self->prepare_fines(undef, undef, $xacts);
+ return $stat if $stat;
+
+ if ($self->cgi->param('BILLTOLASTNAME')) {
+ # We received form1 POST data from the patron.
+ # Request a secure token from PP en route to form 2.
+ # TODO: check for reasonable values for all required fields.
+
+ $self->generate_payflow_secure_token($xacts);
+ }
+
+ return Apache2::Const::OK;
+}
+
+sub payflow_hosted_enabled {
+ my $self = shift;
+ return $self->ctx->{get_org_setting}->(
+ $self->editor->requestor->home_ou,
+ 'credit.processor.payflowhosted.enabled'
+ );
+}
+
+sub payflow_hosted_is_default {
+ my $self = shift;
+ my $org = $self->editor->requestor->home_ou;
+ my $ctx = $self->ctx;
+
+ my $cc_default =
+ $ctx->{get_org_setting}->($org, 'credit.processor.default') || '';
+
+ return $cc_default eq 'PayflowHosted' && $self->payflow_hosted_enabled;
+}
+
+# Generates a PayPal secure token using address, etc. data collected
+# from payflow form1 and adds the necessary data to the template context
+# to direct the user to the hosted payment page.
+sub generate_payflow_secure_token {
+ my $self = shift;
+ my $xacts = shift;
+ my $ctx = $self->ctx;
+ my $cgi = $self->cgi;
+ my $org = $self->editor->requestor->home_ou;
+
+ return unless $self->payflow_hosted_enabled;
+
+ # Collect all BILLTO* params from form1.
+ my %payflow_params =
+ map {$_ => $cgi->param($_)}
+ grep /^BILLTO/, $cgi->param;
+
+ # amount comes from prepare_fines()
+ $payflow_params{AMT} = sprintf("%.2f", $ctx->{fines}->{balance_owed});
+
+ # Generate the PayPal secure token.
+ my $tokens = OpenILS::WWW::EGCatLoader::PayflowHosted::create_xact_token(
+ billing_org => $org,
+ response_host => "https://" . $self->ctx->{hostname},
+ payflow_params => \%payflow_params
+ );
+
+ # The payment form needs these when re-rendering values.
+ $tokens->{payflow_params} = \%payflow_params;
+
+ unless ($tokens && $tokens->{secure_token}) {
+ # Let the template gracefully warn the user.
+ $ctx->{payflow_hosted_ctx} = {init_error => 1};
+ return;
+ }
+
+ # Cache the transactions so the payment can be made internally
+ # after it's made externally.
+ $tokens->{xacts} = $xacts;
+ $tokens->{user} = $self->editor->requestor->id;
+
+ $ctx->{payflow_hosted_ctx} = $tokens;
+
+ # Cache the tokens so we can link the results data from PayPal
+ # back to the original transaction data. Cache time is set to
+ # match PayPal's secure token timeout of 30 minutes.
+ my $cache = OpenSRF::Utils::Cache->new('global');
+ $cache->put_cache($tokens->{secure_token_id}, $tokens, 1800);
+}
+
+# Called from 3rd-party credit card processors to POST payment results data.
+# Caller will not have an authentication token.
+# This happens within an iframe.
+# Either we process the PP POST response data and redirect the user to
+# an intermidiate 'Processing...' page, or if that's already happened,
+# create the payment internally, then let the iframe redirect the
+# browser to the receipts page.
+sub load_myopac_payflow_response {
+ my $self = shift;
+ my $cgi = $self->cgi;
+ my $ctx = $self->ctx;
+
+ my $token_id = $self->ctx->{page_args}->[0];
+
+ if ($token_id) {
+ # Payflow response has already been received.
+ # Create the payment internally now.
+
+ $logger->info("PayflowHosted creating payment for token: $token_id");
+ return $self->payflow_create_payment($token_id);
+
+ } else {
+ # Handling PayFlow POST response. Collect data, put
+ # it into the cache, then redirect the caller back to
+ # this page to finalize processing.
+
+ $logger->info("PayflowHosted processing POST results");
+ return $self->handle_payflow_response;
+ }
+}
+
+sub handle_payflow_response {
+ my $self = shift;
+ my $cgi = $self->cgi;
+ my $ctx = $self->ctx;
+
+ my $respmsg = $cgi->param('RESPMSG');
+ my $order_number = $cgi->param('PNREF');
+ my $appr_code = $cgi->param('AUTHCODE');
+ my $token_id = $cgi->param('SECURETOKENID');
+ my $result = $cgi->param('RESULT');
+
+ # Toss the CGI params into a string for easier error logging.
+ my $log_params = '';
+ $log_params .= "$_=" . $cgi->param($_) . "; " for ($cgi->param);
+
+ $logger->info("PayflowHosted responded with: $log_params");
+
+ if (!$token_id) {
+ $logger->error("PayflowHosted processor responsded with success but ".
+ "failed to return a SECURETOKENID. Cannot complete payment!");
+ $ctx->{payflow_hosted_ctx} = {error => 1};
+ return Apache2::Const::OK;
+ }
+
+ my $cache = OpenSRF::Utils::Cache->new('global');
+ my $tokens = $cache->get_cache($token_id);
+
+ if (!$tokens) {
+
+ $logger->error("PayflowHosted payment succeeded, but no matching ".
+ "transaction data found in memcache. Cannot complete payment!");
+
+ $ctx->{payflow_hosted_ctx} = {
+ error => 1,
+ cc_args => {order_number => $order_number}
+ };
+
+ return Apache2::Const::OK;
+ }
+
+ $tokens->{$_} = $cgi->param($_) for
+ (qw/RESULT PNREF AVSADDR AVSZIP PROCCVV2/);
+
+ $tokens->{pay_result_code} = $result;
+ $ctx->{payflow_hosted_ctx} = $tokens;
+
+ if ($result eq '0') {
+ # Payment processed successfully at PP. Track the payment locally.
+ $logger->info("PayflowHosted processor returned success: $respmsg");
+
+ # Add the completed payment data to the cache.
+ $tokens->{cc_args} = {
+ where_process => -1, # processed externally
+ approval_code => $appr_code,
+ order_number => $order_number,
+ processor => 'PayflowHosted'
+ };
+
+ # Redirect to intermediate 'Processing...' page.
+ $self->ctx->{refresh} = "1; url=pay_response/$token_id";
+ $ctx->{on_processing_page} = 1;
+
+ } else {
+ # Payment failed. Iframe will automatically redirect
+ # back to the form1 page to display the message allow
+ # for a re-pay attempt.
+ if ($result < 0) {
+ $logger->error("PayflowHosted processor returned a".
+ "communication error response code=$result : $respmsg");
+ } else {
+ $logger->warn("PayflowHosted processor returned a non-success ".
+ "(but recoverable) response code=$result : $respmsg");
+ }
+ }
+
+ $cache->put_cache($token_id, $tokens, 1800);
+
+ return Apache2::Const::OK;
+}
+
+sub payflow_create_payment {
+ my ($self, $token_id) = @_;
+
+ my $cgi = $self->cgi;
+ my $ctx = $self->ctx;
+
+ my $cache = OpenSRF::Utils::Cache->new('global');
+ my $tokens = $cache->get_cache($token_id);
+
+ # Must be called before prepare_fines_for_payment();
+ $self->prepare_fines(undef, undef, $tokens->{xacts});
+
+ my $args = {
+ cc_args => $tokens->{cc_args},
+ userid => $tokens->{user},
+ payments => $self->prepare_fines_for_payment,
+ payment_type => "credit_card_payment"
+ };
+
+ $logger->info("PayflowHosted sending payments: ".Dumper($args));
+
+ my $resp = $U->simplereq(
+ "open-ils.circ",
+ "open-ils.circ.money.payment",
+ $self->editor->authtoken, $args,
+ $self->ctx->{user}->last_xact_id
+ );
+
+ $self->ctx->{payment_response} = $resp;
+ $self->ctx->{token_id} = $token_id;
+
+ if ($resp->{textcode}) {
+ $logger->error("PayflowHosted CC internal payment tracking failed");
+ } else {
+ $logger->info("PayflowHosted CC internal payment tracking succeeded");
+ $logger->info("PayflowHosted created payments: ".Dumper($resp->{payments}));
+ $tokens->{payments} = $resp->{payments};
+ }
+
+ $ctx->{payflow_hosted_ctx} = $tokens;
+
+ # Cache the payment info so we can generate a receipt on the receipts
+ # page, but only cache it for a minute so the token can expire.
+ $cache->put_cache($token_id, $tokens, 60);
+
+ return Apache2::Const::OK;
+}
+
+# Generate a printable receipt from a CC payment.
+sub load_myopac_payflow_receipt {
+ my $self = shift;
+
+ my $token_id = $self->ctx->{page_args}->[0];
+ return Apache2::Const::HTTP_BAD_REQUEST unless $token_id;
+
+ my $cache = OpenSRF::Utils::Cache->new('global');
+ my $tokens = $cache->get_cache($token_id);
+
+ # this page is loaded immediately after the token is created.
+ # if the cached data is not there, it's because of an invalid
+ # token (or cache failure) and not because of a timeout.
+ return Apache2::Const::HTTP_BAD_REQUEST
+ unless $tokens && $tokens->{payments};
+
+ $self->ctx->{printable_receipt} = $U->simplereq(
+ "open-ils.circ", "open-ils.circ.money.payment_receipt.print",
+ $self->editor->authtoken, $tokens->{payments}
+ );
+
+ $self->ctx->{payflow_hosted_ctx} = $tokens;
+
+ # NOTE: not deleting the cache data since it's set to expire
+ # within 60 seconds above.
+
+ return Apache2::Const::OK;
+}
+
# TODO: add other filter options as params/configs/etc.
sub load_myopac_payments {
my $self = shift;
pub => 't'
})
);
+
+ # determines which payment form page the user is directed to.
+ $self->ctx->{using_payflow} = $self->payflow_hosted_is_default();
+
return $self->prepare_fines($limit, $offset) || Apache2::Const::OK;
}
--- /dev/null
+package OpenILS::WWW::EGCatLoader::PayflowHosted;
+use strict;
+use warnings;
+use CGI::Util;
+use LWP::UserAgent;
+use UUID::Tiny qw/:std/;
+use OpenSRF::Utils::Logger qw/$logger/;
+my $U = 'OpenILS::Application::AppUtils';
+
+my $test_server = 'https://pilot-payflowpro.paypal.com';
+my $live_server = 'https://payflowpro.paypal.com';
+
+# Hosted Pages server. This is the same for test or live mode.
+my $hosted_server = 'https://payflowlink.paypal.com';
+
+# Creates a transaction token so the calling code can send the user
+# to the hosted pages site.
+#
+# Params (hash):
+#
+# response_host => https://my-host.example.org -- used for RETURNURL, etc.
+# billing_org => org unit for checking credit card AOUS values.
+# payflow_params => {billing params sent to paypal}
+#
+# Returns (hash):
+#
+# secure_token => paypal_generated_token
+# secure_tokan_id => locally_generated_token_id
+# test_mode => true/false if we're operating in test mode
+# server => paypal server where users should be directed
+#
+# Returns undef on error (and logs to error log).
+sub create_xact_token {
+ my %params = @_;
+
+ my %settings = get_settings($params{billing_org});
+ return undef unless %settings;
+
+ my %pf_params = %{$params{payflow_params}};
+
+ # Per-transaction unique token
+ (my $tokenid = create_uuid_as_string(UUID_V4)) =~ s/-//g;
+
+ $pf_params{PARTNER} = $settings{partner};
+ $pf_params{VENDOR} = $settings{vendor};
+ $pf_params{TRXTYPE} = 'S'; # sale
+ $pf_params{TEMPLATE} = 'MINLAYOUT'; # or MOBILE (AKA "Layout C")
+ $pf_params{URLMETHOD} = 'POST';
+ $pf_params{SECURETOKENID} = $tokenid;
+ $pf_params{CREATESECURETOKEN} = 'Y';
+
+ if ($settings{autohosts}) {
+ # Tell PP to send POST response data to this host,
+ # regardless of what's configured within PayPal.
+ my $host = $params{response_host};
+ $pf_params{RETURNURL} = "$host/eg/opac/payflow/pay_response";
+ $pf_params{CANCELURL} = "$host/eg/opac/biblio/main_fines";
+ $pf_params{ERRORURL} = $pf_params{RETURNURL};
+ }
+
+ my $server = $settings{testmode} ? $test_server : $live_server;
+
+ # Log the request to be sent, minus the user and password values.
+ $logger->info("PayflowHosted sending to server $server: ".
+ encode_params(%pf_params));
+
+ # Now that we've logged the params, add the user and password
+ $pf_params{USER} = $settings{login},
+ $pf_params{PWD} = $settings{password},
+
+ my $req = HTTP::Request->new(POST => $server);
+ $req->header('content-type' => 'text/namevalue');
+ $req->content(encode_params(%pf_params));
+
+ my $resp = LWP::UserAgent->new->request($req);
+
+ unless ($resp->is_success) {
+ $logger->error(sprintf(
+ "PayflowHosted HTTPS error code=%s, message=%s",
+ $resp->code, $resp->message
+ ));
+ return undef;
+ }
+
+ my $content = $resp->decoded_content;
+
+ # $content does not contain passwords, etc.
+ $logger->info("PayflowHosted response: $content");
+
+ my %results = parse_response($content);
+
+ unless ($results{SECURETOKEN}) {
+ $logger->error("PayflowHosted failed to return a secure token. ".
+ "Response message => " . $results{RESPMSG});
+
+ return undef;
+ }
+
+ return {
+ secure_token => $results{SECURETOKEN},
+ secure_token_id => $results{SECURETOKENID},
+ test_mode => $settings{testmode},
+ hosted_server => $hosted_server
+ };
+}
+
+# Returns a hash of settings values if the caller should continue
+# (i.e. payflowhosted is used). Returns undef otherwise.
+sub get_settings {
+ my $org = shift;
+
+ my %params;
+ my $spfx = 'credit.processor.payflowhosted';
+
+ for my $p (qw/partner vendor login password testmode enabled autohosts/) {
+ $params{$p} = $U->ou_ancestor_setting_value($org, "$spfx.$p");
+
+ if (!$params{$p} && $p ne 'testmode' && $p ne 'autohosts') {
+ $logger->error("Attempt to make payment via 'payflowhosted' ".
+ "with no value for org unit setting: '$spfx.$p'");
+ return undef;
+ }
+ }
+
+ return %params;
+}
+
+# Business::OnlinePayment::PayflowPro does not support the parameters
+# necessary for Payflow Hosted Pages. It also internally maps POST
+# parameter names from "friendly" names to their PayPal equivalents,
+# making it impossible to pass ad-hoc parameters. Instead of using
+# ::PayflowPro, use its request encoding and response reading code,
+# since that's all we really need.
+
+
+# Body (and comments) for this function copied practically verbatim from submit()
+# http://search.cpan.org/~plobbes/Business-OnlinePayment-PayflowPro-1.01/PayflowPro.pm
+sub encode_params {
+ my %params = @_;
+
+ # Payflow Pro does not use URL encoding for the request. The
+ # following implements their custom encoding scheme. Per the
+ # developer docs, the PARMLIST Syntax Guidelines are:
+ # - Spaces are allowed in values
+ # - Enclose the PARMLIST in quotation marks ("")
+ # - Do not place quotation marks ("") within the body of the PARMLIST
+ # - Separate all PARMLIST name-value pairs using an ampersand (&)
+ #
+ # Because '&' and '=' have special meanings/uses values containing
+ # these special characters must be encoded using a special "length
+ # tag". The "length tag" is simply the length of the "value"
+ # enclosed in square brackets ([]) and appended to the "name"
+ # portion of the name-value pair.
+ #
+ # For more details see the sections 'Using Special Characters in
+ # Values' and 'PARMLIST Syntax Guidelines' in the PayPal Payflow
+ # Pro Developer's Guide
+ return join(
+ '&',
+ map {
+ my $key = $_;
+ my $value = defined( $params{$key} ) ? $params{$key} : '';
+ if ( index( $value, '&' ) != -1 || index( $value, '=' ) != -1 ) {
+ $key = $key . "[" . length($value) . "]";
+ }
+ "$key=$value";
+ } keys %params
+ );
+}
+
+# Body for this function copied heavily from _get_response()
+# http://search.cpan.org/~plobbes/Business-OnlinePayment-PayflowPro-1.01/PayflowPro.pm
+sub parse_response {
+ my $content = shift;
+ my %response;
+ return %response unless $content;
+
+ foreach (split( /[&;]/, $content)) {
+ my ($param, $value) = split('=', $_, 2);
+
+ next unless defined $param;
+ $value = '' unless defined $value;
+
+ $param = CGI::Util::unescape($param);
+ $value = CGI::Util::unescape($value);
+ $response{$param} = $value;
+ }
+
+ return %response;
+}
+
+1;
+
+
+__DATA__
+
+Response blob:
+
+TYPE=S&
+RESPMSG=Approved&
+ACCT=1234&
+COUNTRY=US&
+VISACARDLEVEL=12&
+TAX=0.00&
+CARDTYPE=0&
+PNREF=12341EE308F6&
+TENDER=CC&
+AVSDATA=XXN&
+METHOD=CC&
+SECURETOKEN=123456NYslUGMy0tlKafELwct&
+SHIPTOCOUNTRY=US&
+AMT=40.00&
+SECURETOKENID=12528208de1413abc3d60c86cb15&
+TRANSTIME=2012-03-26+14%3A07%3A59&
+HOSTCODE=A&
+COUNTRYTOSHIP=US&
+RESULT=0&
+AUTHCODE=124PNI&
+
+