JBAS-1503 PayPal Layout-A plus Silent POST
authorBill Erickson <berickxx@gmail.com>
Tue, 9 Aug 2016 15:05:04 +0000 (11:05 -0400)
committerBill Erickson <berickxx@gmail.com>
Thu, 21 Mar 2019 19:46:23 +0000 (15:46 -0400)
Avoid iframe by sending patrons to PP to pay.  PP posts results back to
EG via Silent POST.  Silent POSTs that fail result in a voided payment
on the PP side.

Signed-off-by: Bill Erickson <berickxx@gmail.com>
12 files changed:
KCLS/openils/var/templates_kcls/opac/biblio/main_fines.tt2
KCLS/openils/var/templates_kcls/opac/biblio/main_refund_policy.tt2
KCLS/openils/var/templates_kcls/opac/payflow/errors.tt2
KCLS/openils/var/templates_kcls/opac/payflow/form1.tt2
KCLS/openils/var/templates_kcls/opac/payflow/form2.tt2 [deleted file]
KCLS/openils/var/templates_kcls/opac/payflow/pay_form.tt2
KCLS/openils/var/templates_kcls/opac/payflow/silent_post.tt2 [new file with mode: 0644]
Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader.pm
Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Account.pm
Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/PayflowHosted.pm
Open-ILS/src/templates/opac/css/style.css.tt2
Open-ILS/web/css/skin/default/opac/style.css

index 3bad65b..649c08a 100644 (file)
     pay_form_url = ctx.opac_root _ '/payflow/pay_form';
   END;
 %]
+
+[% IF ctx.payflow_hosted_ctx.pay_result_code %]
+  <!-- Previous payment attempt was rejected by PP -->
+  <div style="background-color:white; width:100%; text-align: center; padding:10px;">
+    [% INCLUDE 'opac/payflow/errors.tt2' %]
+  </div>
+[% END %]
+
 <form action="[% pay_form_url %]" method="GET" style="background:#fff">
     [% IF ctx.fines.circulation.size > 0 %]
     <div id='myopac_circ_trans_div'>
@@ -40,7 +48,8 @@
                     <td>[% l("Date Returned") %]</td>
                     <td>[% l("Balance Owed") %]</td>
                     <td nowrap="nowrap" style="white-space:nowrap;">
-                        <input id="pay_fines_box1" checked="checked"
+                        <input id="pay_fines_box1" 
+                          [% IF NOT ctx.selected_xacts.0 %]checked="checked"[% END %]
                             type="checkbox" onclick="select_all_checkboxes('xact', this.checked)"
                             title="[% l('Click to (un)select all fines') %]" />
                         <label for="pay_fines_box1">[% l('Pay Fines') %]</label>
                         </strong>
                     </td>
                     <td>
-                        <input type="checkbox" checked="checked" 
+                        [% checked = NOT ctx.selected_xacts
+                            || ctx.selected_xacts.grep(f.xact.id).0 %]
+                        <input type="checkbox" 
+                          [% IF checked %]checked="checked"[% END %]
                             title="[% l('Pay this fine') %]" name="xact"
                             value="[% f.xact.id %]" />
                     </td>
                     <td width='16%'>[% l("Billing Type") %]</td>
                     <td width='4%' align="center" nowrap="nowrap"
                         style="white-space:nowrap;">
-                        <input id="pay_fines_box2" checked="checked"
+                        <input id="pay_fines_box2"
+                          [% IF NOT ctx.selected_xacts.0 %]checked="checked"[% END %]
                             type="checkbox" onclick="select_all_checkboxes('xact_misc', this.checked)"
                             title="[% l('Click to (un)select all fines') %]" />
                         <label for="pay_fines_box2">[% l("Pay Fines") %]</label>
                     </td>
                     <td>[% f.xact.last_billing_type %]</td>
                     <td>
-                        <input type="checkbox" title='[% l("Pay this fine") %]'
-                            name="xact_misc" value="[% f.xact.id %]"
-                            checked="checked" />
+                        [% checked = NOT ctx.selected_xacts
+                            || ctx.selected_xacts.grep(f.xact.id).0 %]
+                        <input type="checkbox" 
+                          [% IF checked %]checked="checked"[% END %]
+                          title='[% l("Pay this fine") %]'
+                            name="xact_misc" value="[% f.xact.id %]"/>
                     </td>
                 </tr>
                 [% END %]
index 74755a3..10976fe 100644 (file)
@@ -19,8 +19,6 @@
         non-refundable items, visit
         <a href="http://kcls.org/faq/borrowing/#faq_1766">
           http://kcls.org/faq/borrowing/#faq_1766
-        </a><br /><br />
-        This site uses VeriSign SSL encryption to ensure your
-        privacy.
+        </a><br/>
     </td>
 </tr>
index 15dc23e..7e04c59 100644 (file)
@@ -1,4 +1,10 @@
-<div class="payment-error">
+<div class="payflow-error-container">
+
+<span>There was a problem processing the credit card payment:</span>
+<br/>
+<br/>
+
+<div class="payflow-error-text">
 [% 
 
 # Map PayFlow POST response codes to patron messages.
@@ -36,6 +42,8 @@ SWITCH ctx.payflow_hosted_ctx.RESULT;
   CASE DEFAULT;
     l('An unkown error occurred attempting credit card payment.');
 END;
-
 %]
 </div>
+<br/>
+<span>Click the 'Pay Fines' button to try the payment again.</span>
+</div>
index 93906cb..15102af 100644 (file)
 
-<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" action="[% ctx.payflow_hosted_ctx.forms_server %]">
 
-<form method="POST" id='cc-form'>
+  <input type="hidden" name="SECURETOKEN" 
+    value="[% ctx.payflow_hosted_ctx.secure_token %]"/>
 
-  [% 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 %]
+  <input type="hidden" name="SECURETOKENID" 
+    value="[% ctx.payflow_hosted_ctx.secure_token_id %]"/>
 
   <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>
+    <tr>
+      <input type="submit" value="[% l('Pay') %]"/>
+      <a href="[% mkurl(ctx.opac_root _ '/biblio/main_fines', {}, 1) %]">
+        [% l('Cancel') %]
+      </a>
+      </td>
+    </tr>
   </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>
-
-
diff --git a/KCLS/openils/var/templates_kcls/opac/payflow/form2.tt2 b/KCLS/openils/var/templates_kcls/opac/payflow/form2.tt2
deleted file mode 100644 (file)
index 3c89cbd..0000000
+++ /dev/null
@@ -1,32 +0,0 @@
-<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>
-
index e5d539c..303c905 100644 (file)
 <style>
   #pay-form-container {
     background: #fff;
-    width: 98%;
-    padding: 5px;
+    padding: 10px;
   }
-  #pay-form-div {
-    float:left;
-  }
-  #pay-xacts-div {
-    float:left;
-    margin-left: 20px;
+
+  #paypal-form {
+    font-size: 130%;
   }
-  .cc-form-invalid-field {
-    color: black;
-    background-color: red;
+
+  #paypal-cancel-div {
+    font-size: 120%;
   }
+
+  .myopac_payments_table thead th:first-child { width: auto; }
+  .myopac_payments_table td { padding: 10px }
 </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>
+  <h2>[% l('KCLS only accepts Visa or MasterCard') %]</h2>
+  <br/>
 
   <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' %]
+    [% IF ctx.payflow_hosted_ctx.init_error %]
+      <div class="payment-error">
+        [% l('Error initializing credit card payments.  Unable to make payments at this time.') %]
+      </div>
 
     [% ELSE %]
+      <form id='paypal-form' method="POST" action="[% ctx.payflow_hosted_ctx.forms_server %]">
+        <input type="hidden" name="SECURETOKEN" 
+          value="[% ctx.payflow_hosted_ctx.secure_token %]"/>
 
-      [% 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' %]
+        <input type="hidden" name="SECURETOKENID" 
+          value="[% ctx.payflow_hosted_ctx.secure_token_id %]"/>
 
+        <input type="submit" value="[% l('Pay') %]"/>
+      </form>
     [% END %]
   </div>
+
+  <br/>
+  <div id='paypal-cancel-div'>
+    Click 
+    <i><b>
+      <a href="[% mkurl(ctx.opac_root _ '/biblio/main_fines', {}, 1) %]">
+      [% l('Cancel') %]
+      </a>
+    </b></i>
+    to go back and change your selection.
+  </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/>
diff --git a/KCLS/openils/var/templates_kcls/opac/payflow/silent_post.tt2 b/KCLS/openils/var/templates_kcls/opac/payflow/silent_post.tt2
new file mode 100644 (file)
index 0000000..9f3458a
--- /dev/null
@@ -0,0 +1 @@
+<b>Payflow Silent POST Placeholder Page</b>
index 22d3361..dca6eb2 100644 (file)
@@ -206,6 +206,11 @@ sub load {
         return $self->load_sms_cn if $skip_sms_auth;
     }
 
+    # Payflow silent post responses do not require a login,
+    # because they use a secondary secure token that acts 
+    # as an auth token proxy.
+    return $self->load_myopac_payflow_silent_post if $path =~ m|opac/payflow/silent_post|;
+
     # ----------------------------------------------------------------
     #  Everything below here requires authentication
     # ----------------------------------------------------------------
@@ -230,7 +235,6 @@ sub load {
     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|;
@@ -251,7 +255,6 @@ sub load {
 
     # 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
index d9eb379..2ac8f23 100644 (file)
@@ -1,6 +1,6 @@
 package OpenILS::WWW::EGCatLoader;
 use strict; use warnings;
-use Apache2::Const -compile => qw(OK DECLINED FORBIDDEN HTTP_INTERNAL_SERVER_ERROR REDIRECT HTTP_BAD_REQUEST);
+use Apache2::Const -compile => qw(OK DECLINED FORBIDDEN HTTP_INTERNAL_SERVER_ERROR REDIRECT HTTP_BAD_REQUEST HTTP_REQUEST_TIME_OUT);
 use OpenSRF::Utils::Logger qw/$logger/;
 use OpenILS::Utils::CStoreEditor qw/:funcs/;
 use OpenILS::Utils::Fieldmapper;
@@ -1806,11 +1806,10 @@ sub load_myopac_payment_form {
     return Apache2::Const::OK;
 }
 
-# UI for entering billing address, etc. data.
+# UI for showing summary of fines to pay and form that directs
+# patrons to the PP site for payment.
 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;
@@ -1818,43 +1817,10 @@ sub load_myopac_payflow_form {
     # 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);
-    }
-
+    $self->generate_payflow_secure_token($xacts);
     return Apache2::Const::OK;
 }
 
@@ -1889,27 +1855,15 @@ sub generate_payflow_secure_token {
 
     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});
-
-    $payflow_params{COMMENT1} = 
-        $ctx->{user}->card->barcode if $ctx->{user}->card;
-
     # Generate the PayPal secure token.
     my $tokens = OpenILS::WWW::EGCatLoader::PayflowHosted::create_xact_token(
+        authtoken => $self->editor->authtoken,
+        user => $ctx->{user},
+        amount => $ctx->{fines}->{balance_owed}, # from prepare_fines()
         billing_org => $org,
-        response_host => "https://" . $self->ctx->{hostname},
-        payflow_params => \%payflow_params
+        response_host => "https://" . $self->ctx->{hostname}
     );
 
-    # 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};
@@ -1930,38 +1884,49 @@ sub generate_payflow_secure_token {
     $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 {
+
+# See if the provided authtoken is still valid.  If so, use it.
+# Otherwise, create a temp auth token using open-ils.auth_internal.
+# Returns undef on succcess, server error on failure.
+sub create_tmp_auth {
     my $self = shift;
-    my $cgi = $self->cgi;
-    my $ctx = $self->ctx;
+    my $authtoken = shift;
+    my $user_id = shift;
 
-    my $token_id = $self->ctx->{page_args}->[0];
+    my $e = $self->editor;
 
-    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);
+    $e->authtoken($authtoken);
+    if ($e->checkauth) { # test token and set $e->requestor
+        $logger->info(
+            "PayflowHosted existing authtoken still valid for $user_id"); 
+        return undef;
+    }
 
-    } 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 generating temp auth token for $user_id");
+        
+    my $evt = $U->simplereq(
+        'open-ils.auth_internal',
+        'open-ils.auth_internal.session.create', {
+            user_id => $user_id,
+            login_type => "temp"
+        }
+    );
 
-        $logger->info("PayflowHosted processing POST results");
-        return $self->handle_payflow_response;
+    if ($evt && $evt->{payload} && 
+            ($authtoken = $evt->{payload}->{authtoken})) {
+        $e->authtoken($authtoken);
+        $e->checkauth; # sets $e->requestor
+        return undef;
     }
+
+    $logger->error("PayflowHosted unable to generate temp auth ".
+        "session for user $user_id to complete credit card payment!");
+
+    return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
 }
 
-sub handle_payflow_response {
+
+sub load_myopac_payflow_silent_post {
     my $self = shift;
     my $cgi = $self->cgi;
     my $ctx = $self->ctx;
@@ -1971,6 +1936,7 @@ sub handle_payflow_response {
     my $appr_code = $cgi->param('AUTHCODE');
     my $token_id = $cgi->param('SECURETOKENID');
     my $result = $cgi->param('RESULT');
+    my $authtoken = $cgi->param('USER1');
 
     # Toss the CGI params into a string for easier error logging.
     my $log_params = '';
@@ -1981,31 +1947,24 @@ sub handle_payflow_response {
     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;
+        return Apache2::Const::HTTP_BAD_REQUEST;
     }
 
-    my $cache = OpenSRF::Utils::Cache->new('global');
-    my $tokens = $cache->get_cache($token_id);
+    my $tokens = $self->load_payflow_tokens($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;
+        # Token data was not found in the cache.  
+        # Presumably the cache entry expired.
+        return Apache2::Const::HTTP_REQUEST_TIME_OUT;
     }
 
     $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.
@@ -2019,36 +1978,35 @@ sub handle_payflow_response {
             processor     => 'PayflowHosted'
         };
 
-        # Redirect to intermediate 'Processing...' page.
-        $self->ctx->{refresh} = "1; url=pay_response/$token_id";
-        $ctx->{on_processing_page} = 1;
+        # This API is called directly from PP with no session cookie.
+        # Create a new temp auth session for the paying patron via 
+        # open-ils.auth_internal.
+        my $stat = $self->create_tmp_auth($authtoken, $tokens->{user});
+        return $stat if $stat;
+
+        return $self->payflow_create_payment($token_id, $tokens);
+    }
 
+    # Payment failed.  
+    if ($result < 0) {
+        $logger->error("PayflowHosted processor returned a".
+            "communication error response code=$result : $respmsg");
     } 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");
-        }
+        $logger->warn("PayflowHosted processor returned a non-success ".
+            "(but recoverable) response code=$result : $respmsg");
     }
 
+    # Cache the tokens once again so we can report errors
+    my $cache = OpenSRF::Utils::Cache->new('global');
     $cache->put_cache($token_id, $tokens, 1800);
 
     return Apache2::Const::OK;
 }
 
 sub payflow_create_payment {
-    my ($self, $token_id) = @_;
-
+    my ($self, $token_id, $tokens) = @_;
     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});
@@ -2066,25 +2024,22 @@ sub payflow_create_payment {
         "open-ils.circ", 
         "open-ils.circ.money.payment",
         $self->editor->authtoken, $args, 
-        $self->ctx->{user}->last_xact_id
+        $self->editor->requestor->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};
-    }
+        $logger->error("PayflowHosted CC internal payment tracking ".
+            "failed with event code " . $resp->{textcode});
+        # Tell PP to void the payment.
+        return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
+    } 
 
-    $ctx->{payflow_hosted_ctx} = $tokens;
+    $logger->info("PayflowHosted CC internal payment tracking succeeded");
+    $logger->info("PayflowHosted created payments: ".Dumper($resp->{payments}));
+    $tokens->{payments} = $resp->{payments};
 
-    # 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);
+    # Cache the payment info to generate a receipt on the receipts page.
+    $cache->put_cache($token_id, $tokens, 1800);
 
     return Apache2::Const::OK;
 }
@@ -2096,8 +2051,7 @@ sub load_myopac_payflow_receipt {
     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);
+    my $tokens = $self->load_payflow_tokens($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
@@ -2111,10 +2065,6 @@ sub load_myopac_payflow_receipt {
     );
 
     $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;
 }
 
@@ -2463,8 +2413,10 @@ sub prepare_fines_for_payment {
 
 sub load_myopac_main {
     my $self = shift;
+    my $token_id = $self->ctx->{page_args}->[0];
     my $limit = $self->cgi->param('limit') || 0;
     my $offset = $self->cgi->param('offset') || 0;
+
     $self->ctx->{search_ou} = $self->_get_search_lib();
     $self->ctx->{user}->notes(
         $self->editor->search_actor_usr_note({
@@ -2476,9 +2428,43 @@ sub load_myopac_main {
     # determines which payment form page the user is directed to.
     $self->ctx->{using_payflow} = $self->payflow_hosted_is_default();
 
+    if ($self->ctx->{using_payflow} && $token_id) {
+        # If we have a token_id, it means a payment attempt failed.
+        # Load the cached token data so the template can decide what 
+        # to do next.
+        my $tokens = $self->load_payflow_tokens($token_id);
+
+        if ($tokens) {
+            # Select the transactions in the transaction list that were
+            # used for the failed payment attempt.
+            $self->ctx->{selected_xacts} = $tokens->{xacts};
+
+        } else {
+
+            $logger->error("PayflowHosted payment failed, but we were ".
+                "unable to retrieve the payment context data from the ".
+                "cache. This may be due to the patron taking too long to ".
+                "complete the payment.  Unable to resume payment!");
+
+            $self->ctx->{payflow_hosted_ctx} = {error => 1};
+        }
+    }
+
     return $self->prepare_fines($limit, $offset) || Apache2::Const::OK;
 }
 
+sub load_payflow_tokens {
+    my ($self, $token_id) = @_;
+
+    $logger->info(
+        "PayflowHosted loading cached payment info for token ID $token_id");
+
+    my $cache = OpenSRF::Utils::Cache->new('global');
+    my $tokens = $cache->get_cache($token_id);
+
+    return $self->ctx->{payflow_hosted_ctx} = $tokens;
+}
+
 sub load_myopac_update_email {
     my $self = shift;
     my $e = $self->editor;
index b881399..a80b646 100644 (file)
@@ -7,11 +7,13 @@ 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';
+# API servers
+my $live_api_server = 'https://payflowpro.paypal.com';
+my $test_api_server = 'https://pilot-payflowpro.paypal.com'; 
 
-# Hosted Pages server.  This is the same for test or live mode.
-my $hosted_server = 'https://payflowlink.paypal.com';
+# Hosted Pages servers
+my $live_forms_server = 'https://payflowlink.paypal.com';
+my $test_forms_server = 'https://pilot-payflowlink.paypal.com';
 
 # Creates a transaction token so the calling code can send the user
 # to the hosted pages site.
@@ -36,39 +38,62 @@ sub create_xact_token {
     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;
 
+    my %pf_params;
     $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{TEMPLATE}  = 'TEMPLATEA'; # TODO: just for testing
+    $pf_params{AMT}       = sprintf("%.2f", $params{amount});
+    $pf_params{USER1}     = $params{authtoken};
     $pf_params{SECURETOKENID}     = $tokenid;
     $pf_params{CREATESECURETOKEN} = 'Y';
 
+    my $user = $params{user};
+
+    $pf_params{COMMENT1}        = $user->card->barcode if $user->card;
+    $pf_params{BILLTOFIRSTNAME} = $user->first_given_name;
+    $pf_params{BILLTOLASTNAME}  = $user->family_name;
+    $pf_params{BILLTOEMAIL}     = $user->email;
+    $pf_params{BILLTOPHONE}     = $user->day_phone;
+
+    if (my $addr = $user->billing_address) {
+        $pf_params{BILLTOSTREET} = $addr->street1;
+        $pf_params{BILLTOSTREE2} = $addr->street2;
+        $pf_params{BILLTOCITY}   = $addr->city;
+        $pf_params{BILLTOSTATE}  = $addr->state;
+        $pf_params{BILLTOZIP}    = $addr->post_code;
+    }
+
     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};
+        $pf_params{RETURNURL} = "$host/eg/opac/payflow/pay_receipt/$tokenid";
+        $pf_params{ERRORURL}  = "$host/eg/opac/biblio/main_fines/$tokenid";
+        $pf_params{SILENTPOSTURL} = "$host/eg/opac/payflow/silent_post";
     }
 
-    my $server = $settings{testmode} ? $test_server : $live_server;
+    my $api_server = $live_api_server;
+    my $forms_server = $live_forms_server;
+    if ($settings{testmode}) {
+        $api_server = $test_api_server;
+        $forms_server = $test_forms_server;
+    }
 
     # Log the request to be sent, minus the user and password values.
-    $logger->info("PayflowHosted sending to server $server: ".
+    $logger->info("PayflowHosted sending to server $api_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);
+    my $req = HTTP::Request->new(POST => $api_server);
     $req->header('content-type' => 'text/namevalue');
     $req->content(encode_params(%pf_params));
 
@@ -96,11 +121,16 @@ sub create_xact_token {
         return undef;
     }
 
+    # Avoid leaking sensitive data.
+    delete $pf_params{USER};
+    delete $pf_params{PWD};
+
     return {
         secure_token    => $results{SECURETOKEN},
         secure_token_id => $results{SECURETOKENID},
         test_mode       => $settings{testmode},
-        hosted_server   => $hosted_server
+        forms_server    => $forms_server,
+        payflow_params  => \%pf_params
     };
 }
 
index 1d2a912..15d7527 100644 (file)
@@ -1461,6 +1461,14 @@ div.result_table_utils_cont {
     padding: 10px; border: 1px solid [% css_colors.accent_medium_dark %];
 }
 
+.payflow-error-container {
+    padding: 10px; border: 1px solid [% css_colors.accent_medium_dark %];
+}
+
+.payflow-error-text {
+    font-weight: bold; color: [% css_colors.text_alert %];
+}
+
 .payment-processing {
     font-weight: bold;
     color: [% css_colors.text_greatnews %];
index 76eefb6..68322ef 100644 (file)
@@ -923,6 +923,14 @@ div.select-wrapper:hover {
     padding: 10px; border: 1px solid #888;
 }
 
+.payflow-error-container {
+    padding: 10px; border: 1px solid #888;
+}
+
+.payflow-error-text {
+    font-weight: bold; color: red;
+}
+
 .payment-processing {
     font-weight: bold; color: green;
     font-size: 120%;