From e7418bd3e87db0f6409b42d1296ec009becd59a4 Mon Sep 17 00:00:00 2001 From: Jason Etheridge Date: Thu, 25 Mar 2021 16:21:00 -0400 Subject: [PATCH] lp1894005 Stripe payment intents ===== 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 Signed-off-by: Terran McCanna Signed-off-by: Galen Charlton --- .../perlmods/lib/OpenILS/Application/Circ/Money.pm | 41 +++++++++++++--------- .../lib/OpenILS/WWW/EGCatLoader/Account.pm | 26 ++++++++++++-- Open-ILS/src/sql/Pg/950.data.seed-values.sql | 21 ++++++++++- .../XXXX.data.org-setting-stripe-currency.sql | 26 ++++++++++++++ .../opac/myopac/generic_payment_form.tt2 | 3 -- .../opac/myopac/main_payment_form.tt2 | 10 ++++-- .../opac/myopac/payment_form_error.tt2 | 3 ++ .../opac/myopac/stripe_payment_form.tt2 | 21 ++++++----- .../src/templates-bootstrap/opac/parts/base.tt2 | 2 +- .../opac/parts/myopac/main_refund_policy.tt2 | 11 +----- .../templates/opac/myopac/main_payment_form.tt2 | 14 +++++--- .../templates/opac/myopac/payment_form_error.tt2 | 3 ++ .../templates/opac/myopac/stripe_payment_form.tt2 | 25 +++++++------ Open-ILS/src/templates/opac/parts/base.tt2 | 2 +- .../opac/parts/myopac/main_refund_policy.tt2 | 13 +------ 15 files changed, 149 insertions(+), 72 deletions(-) create mode 100644 Open-ILS/src/sql/Pg/upgrade/XXXX.data.org-setting-stripe-currency.sql create mode 100644 Open-ILS/src/templates-bootstrap/opac/myopac/payment_form_error.tt2 create mode 100644 Open-ILS/src/templates/opac/myopac/payment_form_error.tt2 diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Money.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Money.pm index 6b84a7892e..46f067135a 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Money.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Money.pm @@ -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; diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Account.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Account.pm index f77c1e0a8c..f50a9ceb68 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Account.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Account.pm @@ -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 = { diff --git a/Open-ILS/src/sql/Pg/950.data.seed-values.sql b/Open-ILS/src/sql/Pg/950.data.seed-values.sql index 528ed563e6..0c22e8593d 100644 --- a/Open-ILS/src/sql/Pg/950.data.seed-values.sql +++ b/Open-ILS/src/sql/Pg/950.data.seed-values.sql @@ -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 index 0000000000..64080b2d16 --- /dev/null +++ b/Open-ILS/src/sql/Pg/upgrade/XXXX.data.org-setting-stripe-currency.sql @@ -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; diff --git a/Open-ILS/src/templates-bootstrap/opac/myopac/generic_payment_form.tt2 b/Open-ILS/src/templates-bootstrap/opac/myopac/generic_payment_form.tt2 index 4e68118563..732aed9bab 100644 --- a/Open-ILS/src/templates-bootstrap/opac/myopac/generic_payment_form.tt2 +++ b/Open-ILS/src/templates-bootstrap/opac/myopac/generic_payment_form.tt2 @@ -6,9 +6,6 @@ [% FOR xact IN CGI.param('xact_misc') %] [% END %] - [% IF ctx.use_stripe %] - - [% END %] diff --git a/Open-ILS/src/templates-bootstrap/opac/myopac/main_payment_form.tt2 b/Open-ILS/src/templates-bootstrap/opac/myopac/main_payment_form.tt2 index afb05baebe..a774bb707b 100755 --- a/Open-ILS/src/templates-bootstrap/opac/myopac/main_payment_form.tt2 +++ b/Open-ILS/src/templates-bootstrap/opac/myopac/main_payment_form.tt2 @@ -24,10 +24,14 @@ [% 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 %] 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 index 0000000000..637d22db10 --- /dev/null +++ b/Open-ILS/src/templates-bootstrap/opac/myopac/payment_form_error.tt2 @@ -0,0 +1,3 @@ +
+ [% l('We are unable to process credit card payments at this time. We apologize for the inconvenience. Please contact the library for further assistance.') %] +
diff --git a/Open-ILS/src/templates-bootstrap/opac/myopac/stripe_payment_form.tt2 b/Open-ILS/src/templates-bootstrap/opac/myopac/stripe_payment_form.tt2 index de1a947d96..288991ffef 100644 --- a/Open-ILS/src/templates-bootstrap/opac/myopac/stripe_payment_form.tt2 +++ b/Open-ILS/src/templates-bootstrap/opac/myopac/stripe_payment_form.tt2 @@ -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); - + [% FOR xact IN CGI.param('xact') %] @@ -90,7 +95,7 @@ function build_stripe_form() {
- + [% l('Cancel') %]
diff --git a/Open-ILS/src/templates-bootstrap/opac/parts/base.tt2 b/Open-ILS/src/templates-bootstrap/opac/parts/base.tt2 index 5346346278..2e739d1097 100755 --- a/Open-ILS/src/templates-bootstrap/opac/parts/base.tt2 +++ b/Open-ILS/src/templates-bootstrap/opac/parts/base.tt2 @@ -8,7 +8,7 @@ [% ELSIF ctx.authtime AND !ctx.is_staff %] [% END %] - + diff --git a/Open-ILS/src/templates-bootstrap/opac/parts/myopac/main_refund_policy.tt2 b/Open-ILS/src/templates-bootstrap/opac/parts/myopac/main_refund_policy.tt2 index 350c9a942b..1a30e7a675 100755 --- a/Open-ILS/src/templates-bootstrap/opac/parts/myopac/main_refund_policy.tt2 +++ b/Open-ILS/src/templates-bootstrap/opac/parts/myopac/main_refund_policy.tt2 @@ -1,16 +1,7 @@ diff --git a/Open-ILS/src/templates/opac/myopac/main_payment_form.tt2 b/Open-ILS/src/templates/opac/myopac/main_payment_form.tt2 index 2d60ce6166..ab83442cf6 100644 --- a/Open-ILS/src/templates/opac/myopac/main_payment_form.tt2 +++ b/Open-ILS/src/templates/opac/myopac/main_payment_form.tt2 @@ -20,13 +20,17 @@ [% ELSE %]
[% 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 %] + [% IF ctx.use_stripe %] + [% PROCESS "opac/myopac/stripe_payment_form.tt2"; %] + [% ELSE %] + [% PROCESS "opac/myopac/generic_payment_form.tt2"; %] + [% END %] + [% END %] [% END %]
[% END %] 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 index 0000000000..637d22db10 --- /dev/null +++ b/Open-ILS/src/templates/opac/myopac/payment_form_error.tt2 @@ -0,0 +1,3 @@ +
+ [% l('We are unable to process credit card payments at this time. We apologize for the inconvenience. Please contact the library for further assistance.') %] +
diff --git a/Open-ILS/src/templates/opac/myopac/stripe_payment_form.tt2 b/Open-ILS/src/templates/opac/myopac/stripe_payment_form.tt2 index b282e9c1de..2314545d65 100644 --- a/Open-ILS/src/templates/opac/myopac/stripe_payment_form.tt2 +++ b/Open-ILS/src/templates/opac/myopac/stripe_payment_form.tt2 @@ -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 %] - + [% FOR xact IN CGI.param('xact') %] @@ -96,7 +101,7 @@ function build_stripe_form() { - + [% l('Cancel') %]
-
- [% l('Important! You must have a printed receipt ' _ - 'to be eligible for a refund on lost items ') - %] -
- [% 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.') %]
[% INCLUDE "opac/parts/myopac/main_refund_policy.tt2" %]
diff --git a/Open-ILS/src/templates/opac/parts/base.tt2 b/Open-ILS/src/templates/opac/parts/base.tt2 index 190cf99a51..314ed8aa8a 100644 --- a/Open-ILS/src/templates/opac/parts/base.tt2 +++ b/Open-ILS/src/templates/opac/parts/base.tt2 @@ -8,7 +8,7 @@ [% ELSIF ctx.authtime AND !ctx.is_staff %] [% END %] - + -
- [% l('Important! You must have a printed receipt ' _ - 'to be eligible for a refund on lost items ' _ - '(regulations allow for no exceptions).') - %] -
- [% 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.') %] -- 2.11.0