lp1894005 Stripe payment intents
authorJason Etheridge <jason@EquinoxInitiative.org>
Thu, 25 Mar 2021 20:21:00 +0000 (16:21 -0400)
committerGalen Charlton <gmc@equinoxOLI.org>
Wed, 11 Aug 2021 21:37:16 +0000 (17:37 -0400)
===== Credit card payments using Stripe now implimented with PaymentIntents instead of Charges =====

This changes the Stripe code in the OPAC to use their PaymentIntents and confirmCreditCard API,
which is recommended over their Charges API.  Credit card charges are no longer finalized
(captured/confirmed) on Evergreen's backend, though the backend does check whether a payment was
made successfully before recording it.

Sponsored-by: CW MARS
Sponsored-by: NOBLE

Signed-off-by: Jason Etheridge <jason@EquinoxInitiative.org>
Signed-off-by: Terran McCanna <tmccanna@georgialibraries.org>
Signed-off-by: Galen Charlton <gmc@equinoxOLI.org>

15 files changed:
Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Money.pm
Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Account.pm
Open-ILS/src/sql/Pg/950.data.seed-values.sql
Open-ILS/src/sql/Pg/upgrade/XXXX.data.org-setting-stripe-currency.sql [new file with mode: 0644]
Open-ILS/src/templates-bootstrap/opac/myopac/generic_payment_form.tt2
Open-ILS/src/templates-bootstrap/opac/myopac/main_payment_form.tt2
Open-ILS/src/templates-bootstrap/opac/myopac/payment_form_error.tt2 [new file with mode: 0644]
Open-ILS/src/templates-bootstrap/opac/myopac/stripe_payment_form.tt2
Open-ILS/src/templates-bootstrap/opac/parts/base.tt2
Open-ILS/src/templates-bootstrap/opac/parts/myopac/main_refund_policy.tt2
Open-ILS/src/templates/opac/myopac/main_payment_form.tt2
Open-ILS/src/templates/opac/myopac/payment_form_error.tt2 [new file with mode: 0644]
Open-ILS/src/templates/opac/myopac/stripe_payment_form.tt2
Open-ILS/src/templates/opac/parts/base.tt2
Open-ILS/src/templates/opac/parts/myopac/main_refund_policy.tt2

index 6b84a78..46f0671 100644 (file)
@@ -103,23 +103,32 @@ sub process_stripe_or_bop_payment {
 
     if ($cc_args->{processor} eq 'Stripe') { # Stripe
         my $stripe = Business::Stripe->new(-api_key => $psettings->{secretkey});
-        $stripe->charges_create(
-            amount => ($total_paid * 100), # Stripe takes amount in pennies
-            card => $cc_args->{stripe_token},
-            description => $cc_args->{note}
-        );
-
+        $stripe->api('post','payment_intents/' . $cc_args->{stripe_payment_intent});
         if ($stripe->success) {
-            $logger->info("Stripe payment succeeded");
-            return OpenILS::Event->new(
-                "SUCCESS", payload => {
-                    map { $_ => $stripe->success->{$_} } qw(
-                        invoice customer balance_transaction id created card
-                    )
-                }
-            );
+            $logger->debug('Stripe payment intent retrieved');
+            my $intent = $stripe->success;
+            if ($intent->{status} eq 'succeeded') {
+                $logger->info('Stripe payment succeeded');
+                return OpenILS::Event->new(
+                    'SUCCESS', payload => {
+                        invoice => $intent->{invoice},
+                        customer => $intent->{customer},
+                        balance_transaction => 'N/A',
+                        id => $intent->{id},
+                        created => $intent->{created},
+                        card => 'N/A'
+                    }
+                );
+            } else {
+                $logger->info('Stripe payment failed');
+                return OpenILS::Event->new(
+                    'CREDIT_PROCESSOR_DECLINED_TRANSACTION',
+                    payload => $intent->{last_payment_error}
+                );
+            }
         } else {
-            $logger->info("Stripe payment failed");
+            $logger->debug('Stripe payment intent not retrieved');
+            $logger->info('Stripe payment failed');
             return OpenILS::Event->new(
                 "CREDIT_PROCESSOR_DECLINED_TRANSACTION",
                 payload => $stripe->error  # XXX what happens if this contains
@@ -526,7 +535,7 @@ sub make_payments {
 
         # Urgh, clean up this mega-function one day.
         if ($cc_processor eq 'Stripe' and $approval_code and $cc_payload) {
-            $payment->cc_number($cc_payload->{card}{last4});
+            $payment->cc_number($cc_payload->{card}); # not actually available :)
         }
 
         $payment->approval_code($approval_code) if $approval_code;
index f77c1e0..f50a9ce 100644 (file)
@@ -11,6 +11,7 @@ use OpenSRF::Utils::JSON;
 use OpenSRF::Utils::Cache;
 use OpenILS::Utils::DateTime qw/:datetime/;
 use Digest::MD5 qw(md5_hex);
+use Business::Stripe;
 use Data::Dumper;
 $Data::Dumper::Indent = 0;
 use DateTime;
@@ -2316,8 +2317,29 @@ sub load_myopac_hold_history {
 sub load_myopac_payment_form {
     my $self = shift;
     my $r;
+    my $e = $self->editor;
+
+    $r = $self->prepare_fines(undef, undef, [$self->cgi->param('xact'), $self->cgi->param('xact_misc')]);
+
+    if ( ! $self->cgi->param('last_chance') # only do this once
+        && $self->ctx->{get_org_setting}->($e->requestor->home_ou, 'credit.processor.stripe.enabled')
+        && $self->ctx->{get_org_setting}->($e->requestor->home_ou, 'credit.processor.default') eq 'Stripe') {
+        my $skey = $self->ctx->{get_org_setting}->($e->requestor->home_ou, 'credit.processor.stripe.secretkey');
+        my $currency = $self->ctx->{get_org_setting}->($e->requestor->home_ou, 'credit.processor.stripe.currency');
+        my $stripe = Business::Stripe->new(-api_key => $skey);
+        my $intent = $stripe->api('post', 'payment_intents',
+            amount                => $self->ctx->{fines}->{balance_owed} * 100,
+            currency              => $currency || 'usd'
+        );
+        if ($stripe->success) {
+            $self->ctx->{stripe_client_secret} = $stripe->success()->{client_secret};
+        } else {
+            $logger->error('Error initializing Stripe: ' . Dumper($stripe->error));
+            $self->ctx->{cc_configuration_error} = 1;
+        }
+    }
 
-    $r = $self->prepare_fines(undef, undef, [$self->cgi->param('xact'), $self->cgi->param('xact_misc')]) and return $r;
+    if ($r) { return $r; }
     $r = $self->prepare_extended_user_info and return $r;
 
     return Apache2::Const::OK;
@@ -2384,7 +2406,7 @@ sub load_myopac_pay_init {
     $cc_args->{$_} = $self->cgi->param($_) for (qw/
         number cvv2 expire_year expire_month billing_first
         billing_last billing_address billing_city billing_state
-        billing_zip stripe_token
+        billing_zip stripe_payment_intent stripe_client_secret
     /);
 
     my $cache_args = {
index 528ed56..0c22e85 100644 (file)
@@ -21662,7 +21662,6 @@ VALUES
      'coust', 'description'),
    'integer' );
 
-
 INSERT INTO config.workstation_setting_type (name, grp, datatype, label)
 VALUES (
     'eg.staff.catalog.results.show_more', 'gui', 'bool',
@@ -21673,3 +21672,23 @@ VALUES (
     )
 );
 
+INSERT INTO config.org_unit_setting_type
+    (grp, name, datatype, label, description, update_perm, view_perm)
+VALUES (
+    'credit',
+    'credit.processor.stripe.currency', 'string',
+    oils_i18n_gettext(
+        'credit.processor.stripe.currency',
+        'Stripe ISO 4217 currency code',
+        'coust',
+        'label'
+    ),
+    oils_i18n_gettext(
+        'credit.processor.stripe.currency',
+        'Use an all lowercase version of a Stripe-supported ISO 4217 currency code.  Defaults to "usd"',
+        'coust',
+        'description'
+    ),
+    (SELECT id FROM permission.perm_list WHERE code = 'ADMIN_CREDIT_CARD_PROCESSING'),
+    (SELECT id FROM permission.perm_list WHERE code = 'VIEW_CREDIT_CARD_PROCESSING')
+);
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.data.org-setting-stripe-currency.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.data.org-setting-stripe-currency.sql
new file mode 100644 (file)
index 0000000..64080b2
--- /dev/null
@@ -0,0 +1,26 @@
+BEGIN;
+
+SELECT evergreen.upgrade_deps_block_check('XXXX', :eg_version);
+
+INSERT INTO config.org_unit_setting_type
+    (grp, name, datatype, label, description, update_perm, view_perm)
+VALUES (
+    'credit',
+    'credit.processor.stripe.currency', 'string',
+    oils_i18n_gettext(
+        'credit.processor.stripe.currency',
+        'Stripe ISO 4217 currency code',
+        'coust',
+        'label'
+    ),
+    oils_i18n_gettext(
+        'credit.processor.stripe.currency',
+        'Use an all lowercase version of a Stripe-supported ISO 4217 currency code.  Defaults to "usd"',
+        'coust',
+        'description'
+    ),
+    (SELECT id FROM permission.perm_list WHERE code = 'ADMIN_CREDIT_CARD_PROCESSING'),
+    (SELECT id FROM permission.perm_list WHERE code = 'VIEW_CREDIT_CARD_PROCESSING')
+);
+
+COMMIT;
index 4e68118..732aed9 100644 (file)
@@ -6,9 +6,6 @@
         [% FOR xact IN CGI.param('xact_misc') %]
         <input type="hidden" name="xact_misc" value="[% xact | html %]" />
         [% END %]
-        [% IF ctx.use_stripe %]
-        <input type="hidden" name="stripe_token" id="stripe_token" />
-        [% END %]
 
          <table id="billing_info_table" class="table table-hover">
          <thead>
index afb05ba..a774bb7 100755 (executable)
     [% IF last_chance %]
       [% PROCESS "opac/myopac/last_chance_form.tt2"; %]
     [% ELSE %]
-        [% IF ctx.use_stripe %]
-            [% PROCESS "opac/myopac/stripe_payment_form.tt2"; %]
+        [% IF ctx.cc_configuration_error %]
+            [% PROCESS "opac/myopac/payment_form_error.tt2"; %]
         [% ELSE %]
-            [% PROCESS "opac/myopac/generic_payment_form.tt2"; %]
+            [% IF ctx.use_stripe %]
+                [% PROCESS "opac/myopac/stripe_payment_form.tt2"; %]
+            [% ELSE %]
+                [% PROCESS "opac/myopac/generic_payment_form.tt2"; %]
+            [% END %]
         [% END %]
     [% END %]
 </div></div>
diff --git a/Open-ILS/src/templates-bootstrap/opac/myopac/payment_form_error.tt2 b/Open-ILS/src/templates-bootstrap/opac/myopac/payment_form_error.tt2
new file mode 100644 (file)
index 0000000..637d22d
--- /dev/null
@@ -0,0 +1,3 @@
+<div class="warning_box">
+    <big><strong>[% l('We are unable to process credit card payments at this time. We apologize for the inconvenience. Please contact the library for further assistance.') %]</strong></big>
+</div>
index de1a947..288991f 100644 (file)
@@ -45,32 +45,37 @@ function build_stripe_form() {
     form.addEventListener('submit', function(event) {
       event.preventDefault();
 
-      stripe.createToken(card).then(function(result) {
+      stripe.confirmCardPayment('[% ctx.stripe_client_secret %]',{ payment_method: { card: card} }).then(function(result) {
         if (result.error) {
           // Inform the user if there was an error.
           var errorElement = document.getElementById('card-errors');
           errorElement.textContent = result.error.message;
         } else {
-          // Send the token to your server.
-          stripeTokenHandler(result.token);
+          // Send the payment intent to your server.
+          stripePaymentIntentHandler(result.paymentIntent);
         }
       });
     });
 
-    function stripeTokenHandler(token) {
+    function stripePaymentIntentHandler(payment_intent) {
       var form = document.getElementById('payment_form');
       var hiddenInput = document.createElement('input');
       hiddenInput.setAttribute('type', 'hidden');
-      hiddenInput.setAttribute('name', 'stripe_token');
-      hiddenInput.setAttribute('value', token.id);
+      hiddenInput.setAttribute('name', 'stripe_payment_intent');
+      hiddenInput.setAttribute('value', payment_intent.id);
       form.appendChild(hiddenInput);
+      var hiddenInput2 = document.createElement('input');
+      hiddenInput2.setAttribute('type', 'hidden');
+      hiddenInput2.setAttribute('name', 'stripe_client_secret');
+      hiddenInput2.setAttribute('value', payment_intent.client_secret);
+      form.appendChild(hiddenInput2);
 
       form.submit();
     }
 }
     $(document).ready(build_stripe_form);
 </script>
-<form action="#payment" method="post" id="payment_form">
+<form action="[% ctx.opac_root %]/myopac/main_pay_init" method="post" id="payment_form">
   <input type="hidden" name="last_chance" value="1" />
   [% FOR xact IN CGI.param('xact') %]
   <input type="hidden" name="xact" value="[% xact | html %]" />
@@ -90,7 +95,7 @@ function build_stripe_form() {
     <div id="card-errors" role="alert"></div>
   </div>
   <div id="payment_actions">
-    <button type="submit" id="payment_submit" class="btn btn-confirm"><i class="fas fa-arrow-circle-right"></i> [% l('Next') %]</button>
+    <button type="submit" id="payment_submit" class="btn btn-confirm"><i class="fas fa-arrow-circle-right"></i> [% l('Submit Payment') %]</button>
     <a href="[% mkurl(ctx.opac_root _ '/myopac/main', {}, 1) %]" class="btn btn-deny"><i class="fas fa-ban"></i> [% l('Cancel') %]</a>
   </div>
 
index 5346346..2e739d1 100755 (executable)
@@ -8,7 +8,7 @@
         [% ELSIF ctx.authtime AND !ctx.is_staff %]
         <meta http-equiv="refresh" content="[% ctx.authtime %]; url=[% ctx.home_page %]" />
         [% END %]
-        <meta name = "viewport" content = "initial-scale = 1.0">
+        <meta name = "viewport" content = "width=device-width, initial-scale = 1.0">
         <!--Added bootstrap dependancies-->
         <link rel="stylesheet" href="[% ctx.media_prefix %]/opac/deps/node_modules/bootstrap/dist/css/bootstrap.min.css[% ctx.cache_key %]">
         <link rel="stylesheet"  href="[% ctx.media_prefix %]/opac/deps/node_modules/@fortawesome/fontawesome-free/css/all.css[% ctx.cache_key %]" />
index 350c9a9..1a30e7a 100755 (executable)
@@ -1,16 +1,7 @@
 <tr>
     <td colspan="3">
-        <br />
-        [% l('Important! You must have a printed receipt ' _
-             'to be eligible for a refund on lost items ')
-        %]
-        <br />
         <strong>
-        [% l('Be sure there is an email address on your account ' _
-             'if you would like a receipt to be emailed to you. ' _
-             'Otherwise, make certain you have a printed receipt ' _
-             'in hand before closing the payment receipt screen.')
-        %]
+        [% l('Please print or save the receipt for your records before closing the payment screen.') %]
         </strong>
     </td>
 </tr>
index 2d60ce6..ab83442 100644 (file)
 [% ELSE %]
 <div id="pay_fines_now">
     [% IF last_chance %]
-        [% PROCESS "opac/myopac/last_chance_form.tt2"; %]
+      [% PROCESS "opac/myopac/last_chance_form.tt2"; %]
     [% ELSE %]
-        [% IF ctx.use_stripe %]
-            [% PROCESS "opac/myopac/stripe_payment_form.tt2"; %]
+        [% IF ctx.cc_configuration_error %]
+            [% PROCESS "opac/myopac/payment_form_error.tt2"; %]
         [% ELSE %]
-            [% PROCESS "opac/myopac/generic_payment_form.tt2"; %]
-        [% END %] <!-- of IF ctx.use_stripe -->
+            [% IF ctx.use_stripe %]
+                [% PROCESS "opac/myopac/stripe_payment_form.tt2"; %]
+            [% ELSE %]
+                [% PROCESS "opac/myopac/generic_payment_form.tt2"; %]
+            [% END %]
+        [% END %]
     [% END %]
 </div>
 [% END %] <!-- of IF ctx.fines.balance_owed <= 0 -->
diff --git a/Open-ILS/src/templates/opac/myopac/payment_form_error.tt2 b/Open-ILS/src/templates/opac/myopac/payment_form_error.tt2
new file mode 100644 (file)
index 0000000..637d22d
--- /dev/null
@@ -0,0 +1,3 @@
+<div class="warning_box">
+    <big><strong>[% l('We are unable to process credit card payments at this time. We apologize for the inconvenience. Please contact the library for further assistance.') %]</strong></big>
+</div>
index b282e9c..2314545 100644 (file)
@@ -41,29 +41,34 @@ function build_stripe_form() {
         try { card.focus(); } catch(E) { console.log('failed to focus card element',E); }
     });
 
-    var form = document.getElementById('payment-form');
+    var form = document.getElementById('payment_form');
     form.addEventListener('submit', function(event) {
       event.preventDefault();
 
-      stripe.createToken(card).then(function(result) {
+      stripe.confirmCardPayment('[% ctx.stripe_client_secret %]',{ payment_method: { card: card} }).then(function(result) {
         if (result.error) {
           // Inform the user if there was an error.
           var errorElement = document.getElementById('card-errors');
           errorElement.textContent = result.error.message;
         } else {
-          // Send the token to your server.
-          stripeTokenHandler(result.token);
+          // Send the payment intent to your server.
+          stripePaymentIntentHandler(result.paymentIntent);
         }
       });
     });
 
-    function stripeTokenHandler(token) {
-      var form = document.getElementById('payment-form');
+    function stripePaymentIntentHandler(payment_intent) {
+      var form = document.getElementById('payment_form');
       var hiddenInput = document.createElement('input');
       hiddenInput.setAttribute('type', 'hidden');
-      hiddenInput.setAttribute('name', 'stripe_token');
-      hiddenInput.setAttribute('value', token.id);
+      hiddenInput.setAttribute('name', 'stripe_payment_intent');
+      hiddenInput.setAttribute('value', payment_intent.id);
       form.appendChild(hiddenInput);
+      var hiddenInput2 = document.createElement('input');
+      hiddenInput2.setAttribute('type', 'hidden');
+      hiddenInput2.setAttribute('name', 'stripe_client_secret');
+      hiddenInput2.setAttribute('value', payment_intent.client_secret);
+      form.appendChild(hiddenInput2);
 
       form.submit();
     }
@@ -76,7 +81,7 @@ function build_stripe_form() {
     setTimeout(build_stripe_form,0);
 [% END %]
 </script>
-<form action="#payment" method="post" id="payment-form">
+<form action="[% ctx.opac_root %]/myopac/main_pay_init" method="post" id="payment_form">
   <input type="hidden" name="last_chance" value="1" />
   [% FOR xact IN CGI.param('xact') %]
   <input type="hidden" name="xact" value="[% xact | html %]" />
@@ -96,7 +101,7 @@ function build_stripe_form() {
     <div id="card-errors" role="alert"></div>
   </div>
 
-  <button class="opac-button">Next</button>
+  <button class="opac-button">[% l('Submit Payment') %]</button>
   <a href="[% mkurl(ctx.opac_root _ '/myopac/main', {}, 1) %]" class="opac-button">[% l('Cancel') %]</a> 
 </form>
 <table>[% INCLUDE "opac/parts/myopac/main_refund_policy.tt2" %]</table>
index 190cf99..314ed8a 100644 (file)
@@ -8,7 +8,7 @@
         [% ELSIF ctx.authtime AND !ctx.is_staff %]
         <meta http-equiv="refresh" content="[% ctx.authtime %]; url=[% ctx.home_page %]" />
         [% END %]
-        <meta name = "viewport" content = "initial-scale = 1.0">
+        <meta name = "viewport" content = "width=device-width, initial-scale = 1.0">
         <link rel="stylesheet" type="text/css" href="[% ctx.media_prefix %]/css/skin/default/opac/semiauto.css[% ctx.cache_key %]" />
         <link rel="stylesheet" type="text/css" href="[% ctx.opac_root %]/css/style.css[% ctx.cache_key %]&amp;dir=[%
           IF ctx.get_i18n_l(ctx.eg_locale).rtl == 't' %]rtl[%
index d020722..1a30e7a 100644 (file)
@@ -1,18 +1,7 @@
 <tr>
     <td colspan="3">
-        <br />
-        [% l('Important! You must have a printed receipt ' _
-             'to be eligible for a refund on lost items ' _
-             '(regulations allow for no exceptions).')
-        %]
-        <br />
         <strong>
-        [% l('To ensure your necessary receipt information ' _
-             'is not lost, enter your email address above ' _
-             'and a receipt will be emailed to you. Otherwise, ' _
-             'make certain you have a printed receipt in hand ' _
-             'before closing the payment receipt screen.')
-        %]
+        [% l('Please print or save the receipt for your records before closing the payment screen.') %]
         </strong>
     </td>
 </tr>