===== 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>
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
# 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;
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;
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;
$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 = {
'coust', 'description'),
'integer' );
-
INSERT INTO config.workstation_setting_type (name, grp, datatype, label)
VALUES (
'eg.staff.catalog.results.show_more', 'gui', 'bool',
)
);
+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')
+);
--- /dev/null
+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;
[% 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>
[% 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>
--- /dev/null
+<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>
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 %]" />
<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>
[% 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 %]" />
<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>
[% 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 -->
--- /dev/null
+<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>
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();
}
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 %]" />
<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>
[% 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 %]&dir=[%
IF ctx.get_i18n_l(ctx.eg_locale).rtl == 't' %]rtl[%
<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>