From af47ebafaaca04aef6fe00d01bcd6278eb3e3c8a Mon Sep 17 00:00:00 2001 From: Lebbeous Fogle-Weekley Date: Thu, 19 Sep 2013 11:00:00 -0400 Subject: [PATCH] Support Stripe payments with some new code and some rearranged code ... ... behind the open-ils.circ.money.payment method. - Also add Business::Stripe as a CPAN module to the pre-reqs installers. - If payment processor is Stripe, only show form if Javascript enabled. Our implementation of payments via Stripe doesn't work without Javascript. That's part of the point. Using Stripe really limits a site's worries about PCI compliance because users' credit card number and security codes are never transmitted to the [Evergreen] server at all. That data goes to Stripe instead (using Javascript) and from that we get back a one-time token from Stripe to give to our server instead. Thanks to Jason Boyer at the Indiana State Library for recognizing the value of supporting approaches like that in Evergreen and for starting the work. Signed-off-by: Lebbeous Fogle-Weekley Conflicts: Open-ILS/src/templates/opac/myopac/main_payment_form.tt2 Signed-off-by: Remington Steed Signed-off-by: Dan Wells --- .../src/extras/install/Makefile.debian-squeeze | 1 + Open-ILS/src/extras/install/Makefile.debian-wheezy | 1 + Open-ILS/src/extras/install/Makefile.fedora | 1 + Open-ILS/src/extras/install/Makefile.ubuntu-lucid | 1 + .../src/extras/install/Makefile.ubuntu-precise | 1 + .../lib/OpenILS/Application/Circ/CreditCard.pm | 38 +---- .../perlmods/lib/OpenILS/Application/Circ/Money.pm | 182 +++++++++++++++++---- .../lib/OpenILS/WWW/EGCatLoader/Account.pm | 2 +- Open-ILS/src/templates/opac/myopac/main_pay.tt2 | 3 +- .../templates/opac/myopac/main_payment_form.tt2 | 33 ++-- Open-ILS/src/templates/opac/parts/base.tt2 | 4 +- Open-ILS/src/templates/opac/parts/js.tt2 | 4 + Open-ILS/src/templates/opac/parts/stripe.tt2 | 9 +- 13 files changed, 189 insertions(+), 91 deletions(-) diff --git a/Open-ILS/src/extras/install/Makefile.debian-squeeze b/Open-ILS/src/extras/install/Makefile.debian-squeeze index 8439fdeb7a..69f91515c7 100644 --- a/Open-ILS/src/extras/install/Makefile.debian-squeeze +++ b/Open-ILS/src/extras/install/Makefile.debian-squeeze @@ -67,6 +67,7 @@ export DEB_APACHE_DISMODS = \ export CPAN_MODULES = \ Business::OnlinePayment::PayPal \ + Business::Stripe \ Library::CallNumber::LC \ Net::Z3950::Simple2ZOOM \ RPC::XML \ diff --git a/Open-ILS/src/extras/install/Makefile.debian-wheezy b/Open-ILS/src/extras/install/Makefile.debian-wheezy index 755acd0d64..e4634d3d3e 100644 --- a/Open-ILS/src/extras/install/Makefile.debian-wheezy +++ b/Open-ILS/src/extras/install/Makefile.debian-wheezy @@ -70,6 +70,7 @@ export DEB_APACHE_DISMODS = \ export CPAN_MODULES = \ Business::OnlinePayment::PayPal \ + Business::Stripe \ Template::Plugin::POSIX \ Safe diff --git a/Open-ILS/src/extras/install/Makefile.fedora b/Open-ILS/src/extras/install/Makefile.fedora index d70e455daf..0149387eb4 100644 --- a/Open-ILS/src/extras/install/Makefile.fedora +++ b/Open-ILS/src/extras/install/Makefile.fedora @@ -64,6 +64,7 @@ export CPAN_MODULES = \ Business::ISSN \ Net::Z3950::ZOOM \ Net::Z3950::Simple2ZOOM \ + Business::Stripe \ Template::Plugin::POSIX \ SRU \ Rose::URI diff --git a/Open-ILS/src/extras/install/Makefile.ubuntu-lucid b/Open-ILS/src/extras/install/Makefile.ubuntu-lucid index 02829b3d05..5400bcc9f7 100644 --- a/Open-ILS/src/extras/install/Makefile.ubuntu-lucid +++ b/Open-ILS/src/extras/install/Makefile.ubuntu-lucid @@ -64,6 +64,7 @@ export DEB_APACHE_DISMODS = \ export CPAN_MODULES = \ Business::ISSN \ Business::OnlinePayment::PayPal \ + Business::Stripe \ Library::CallNumber::LC \ MARC::Record \ Net::Z3950::Simple2ZOOM \ diff --git a/Open-ILS/src/extras/install/Makefile.ubuntu-precise b/Open-ILS/src/extras/install/Makefile.ubuntu-precise index b6f324373b..2418a01fba 100644 --- a/Open-ILS/src/extras/install/Makefile.ubuntu-precise +++ b/Open-ILS/src/extras/install/Makefile.ubuntu-precise @@ -72,6 +72,7 @@ export DEB_APACHE_DISMODS = \ export CPAN_MODULES = \ Business::CreditCard::Object \ + Business::Stripe \ Business::OnlinePayment::PayPal \ Template::Plugin::POSIX \ Rose::URI \ diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/CreditCard.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/CreditCard.pm index 6ac63a7476..2c6183077e 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/CreditCard.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/CreditCard.pm @@ -30,8 +30,6 @@ use OpenILS::Utils::CStoreEditor qw/:funcs/; use OpenILS::Application::AppUtils; my $U = "OpenILS::Application::AppUtils"; -use constant CREDIT_NS => "credit"; - # Given the argshash from process_payment(), this helper function just finds # a function in the current namespace named "bop_args_{processor}" and calls # it with $argshash as an argument, returning the result, or returning an @@ -78,18 +76,6 @@ sub bop_args_PayflowPro { ); } -sub get_processor_settings { - my $org_unit = shift; - my $processor = lc shift; - - # XXX TODO: make this one single cstore request instead of many - +{ map { ($_ => - $U->ou_ancestor_setting_value( - $org_unit, CREDIT_NS . ".processor.${processor}.${_}" - )) } qw/enabled login password signature server testmode vendor partner/ - }; -} - # argshash (Hash of arguments with these keys): # patron_id: Not a barcode, but a patron's internal ID # ou: Org unit where transaction happens @@ -119,29 +105,7 @@ sub process_payment { and $argshash->{expiration} and $argshash->{ou}; - if (!$argshash->{processor}) { - if (!($argshash->{processor} = - $U->ou_ancestor_setting_value( - $argshash->{ou}, CREDIT_NS . '.processor.default'))) { - return OpenILS::Event->new('CREDIT_PROCESSOR_NOT_SPECIFIED'); - } - } - # Basic sanity check on processor name. - if ($argshash->{processor} !~ /^[a-z0-9_\-]+$/i) { - return OpenILS::Event->new('CREDIT_PROCESSOR_NOT_ALLOWED'); - } - - # Get org unit settings related to our processor - my $psettings = get_processor_settings( - $argshash->{ou}, $argshash->{processor} - ); - - if (!$psettings->{enabled}) { - return OpenILS::Event->new('CREDIT_PROCESSOR_NOT_ENABLED'); - } - - # Add the org unit settings for the chosen processor to our argshash. - $argshash = +{ %{$argshash}, %{$psettings} }; + # Used to test argshash->{processor} here, but now that's handled earlier. # At least the following (derived from org unit settings) are required. return OpenILS::Event->new('CREDIT_PROCESSOR_BAD_PARAMS') 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 a6220edaf9..be2c88b210 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Money.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Money.pm @@ -27,8 +27,128 @@ use OpenILS::Event; use OpenSRF::Utils::Logger qw/:logger/; use OpenILS::Utils::CStoreEditor qw/:funcs/; use OpenILS::Utils::Penalty; +use Business::Stripe; $Data::Dumper::Indent = 0; +sub get_processor_settings { + my $e = shift; + my $org_unit = shift; + my $processor = lc shift; + + # Get the names of every credit processor setting for our given processor. + # They're a little different per processor. + my $setting_names = $e->json_query({ + select => {coust => ["name"]}, + from => {coust => {}}, + where => {name => {like => "credit.processor.${processor}.%"}} + }) or return $e->die_event; + + # Make keys for a hash we're going to build out of the last dot-delimited + # component of each setting name. + ($_->{key} = $_->{name}) =~ s/.+\.(\w+)$/$1/ for @$setting_names; + + # Return a hash with those short keys, and for values the value of + # the corresponding OU setting within our scope. + return { + map { + $_->{key} => $U->ou_ancestor_setting_value($org_unit, $_->{name}) + } @$setting_names + }; +} + +# process_stripe_or_bop_payment() +# This is a helper method to make_payments() below (specifically, +# the credit-card part). It's the first point in the Perl code where +# we need to care about the distinction between Stripe and the +# Paypal/PayflowPro/AuthorizeNet kinds of processors (the latter group +# uses B::OP and handles payment card info, whereas Stripe doesn't use +# B::OP and doesn't require us to know anything about the payment card +# info). +# +# Return an event in all cases. That means a success returns a SUCCESS +# event. +sub process_stripe_or_bop_payment { + my ($e, $user_id, $this_ou, $total_paid, $cc_args) = @_; + + # A few stanzas to determine which processor we're using and whether we're + # really adequately set up for it. + if (!$cc_args->{processor}) { + if (!($cc_args->{processor} = + $U->ou_ancestor_setting_value( + $this_ou, 'credit.processor.default' + ) + ) + ) { + return OpenILS::Event->new('CREDIT_PROCESSOR_NOT_SPECIFIED'); + } + } + + # Make sure the configured credit processor has a safe/correct name. + return OpenILS::Event->new('CREDIT_PROCESSOR_NOT_ALLOWED') + unless $cc_args->{processor} =~ /^[a-z0-9_\-]+$/i; + + # Get the settings for the processor and make sure they're serviceable. + my $psettings = get_processor_settings($e, $this_ou, $cc_args->{processor}); + return $psettings if defined $U->event_code($psettings); + return OpenILS::Event->new('CREDIT_PROCESSOR_NOT_ENABLED') + unless $psettings->{enabled}; + + # Now we branch. Stripe is one thing, and everything else is another. + + if ($cc_args->{processor} eq 'Stripe') { # Stripe + my $stripe = Business::Stripe->new(-api_key => $psettings->{secretkey}); + $stripe->charges_create( + amount => int($total_paid * 100.0), # Stripe takes amount in pennies + card => $cc_args->{stripe_token}, + description => $cc_args->{note} + ); + + 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 + ) + } + ); + } else { + $logger->info("Stripe payment failed"); + return OpenILS::Event->new( + "CREDIT_PROCESSOR_DECLINED_TRANSACTION", + payload => $stripe->error # XXX what happens if this contains + # JSON::backportPP::* objects? + ); + } + + } else { # B::OP style (Paypal/PayflowPro/AuthorizeNet) + return OpenILS::Event->new('BAD_PARAMS', note => 'Need CC number') + unless $cc_args->{number}; + + return OpenILS::Application::Circ::CreditCard::process_payment({ + "desc" => $cc_args->{note}, + "amount" => $total_paid, + "patron_id" => $user_id, + "cc" => $cc_args->{number}, + "expiration" => sprintf( + "%02d-%04d", + $cc_args->{expire_month}, + $cc_args->{expire_year} + ), + "ou" => $this_ou, + "first_name" => $cc_args->{billing_first}, + "last_name" => $cc_args->{billing_last}, + "address" => $cc_args->{billing_address}, + "city" => $cc_args->{billing_city}, + "state" => $cc_args->{billing_state}, + "zip" => $cc_args->{billing_zip}, + "cvv2" => $cc_args->{cvv2}, + %$psettings + }); + + } +} + __PACKAGE__->register_method( method => "make_payments", api_name => "open-ils.circ.money.payment", @@ -49,6 +169,7 @@ __PACKAGE__->register_method( approval_code (for out-of-band payment) type (for out-of-band payment) number (for call to payment processor) + stripe_token (for call to Stripe payment processor) expire_month (for call to payment processor) expire_year (for call to payment processor) billing_first (for out-of-band payments and for call to payment processor) @@ -264,7 +385,7 @@ sub make_payments { if ($payobj->has_field('cc_number')) { $payobj->cc_number(substr($cc_args->{number}, -4)); } - if ($payobj->has_field('expire_month')) { $payobj->expire_month($cc_args->{expire_month}); } + if ($payobj->has_field('expire_month')) { $payobj->expire_month($cc_args->{expire_month}); $logger->info("LFW XXX expire_month is $cc_args->{expire_month}"); } if ($payobj->has_field('expire_year')) { $payobj->expire_year($cc_args->{expire_year}); } # Note: It is important not to set approval_code @@ -289,46 +410,34 @@ sub make_payments { # If an approval code was not given, we'll need # to call to the payment processor ourselves. if ($cc_args->{where_process} == 1) { - return OpenILS::Event->new('BAD_PARAMS', note => 'Need CC number') - if not $cc_args->{number}; - my $response = - OpenILS::Application::Circ::CreditCard::process_payment({ - "desc" => $cc_args->{note}, - "amount" => $total_paid, - "patron_id" => $user_id, - "cc" => $cc_args->{number}, - "expiration" => sprintf( - "%02d-%04d", - $cc_args->{expire_month}, - $cc_args->{expire_year} - ), - "ou" => $this_ou, - "first_name" => $cc_args->{billing_first}, - "last_name" => $cc_args->{billing_last}, - "address" => $cc_args->{billing_address}, - "city" => $cc_args->{billing_city}, - "state" => $cc_args->{billing_state}, - "zip" => $cc_args->{billing_zip}, - "cvv2" => $cc_args->{cvv2}, - }); - - if ($U->event_code($response)) { # non-success + my $response = process_stripe_or_bop_payment( + $e, $user_id, $this_ou, $total_paid, $cc_args + ); + + if ($U->event_code($response)) { # non-success (success is 0) $logger->info( "Credit card payment for user $user_id failed: " . - $response->{"textcode"} . " " . - $response->{"payload"}->{"error_message"} + $response->{textcode} . " " . + ($response->{payload}->{error_message} || + $response->{payload}{message}) ); - return $response; } else { # We need to save this for later in case there's a failure on # the EG side to store the processor's result. - $cc_payload = $response->{"payload"}; - $approval_code = $cc_payload->{"authorization"}; - $cc_type = $cc_payload->{"card_type"}; - $cc_processor = $cc_payload->{"processor"}; - $cc_order_number = $cc_payload->{"order_number"}; + $cc_payload = $response->{"payload"}; # also used way later + + { + no warnings 'uninitialized'; + $cc_type = $cc_payload->{card_type}; + $approval_code = $cc_payload->{authorization} || + $cc_payload->{id}; + $cc_processor = $cc_payload->{processor} || + $cc_args->{processor}; + $cc_order_number = $cc_payload->{order_number} || + $cc_payload->{invoice}; + }; $logger->info("Credit card payment for user $user_id succeeded"); } } else { @@ -374,6 +483,13 @@ sub make_payments { } } + # Urgh, clean up this mega-function one day. + if ($cc_processor eq 'Stripe' and $approval_code and $cc_payload) { + $payment->expire_month($cc_payload->{card}{exp_month}); + $payment->expire_year($cc_payload->{card}{exp_year}); + $payment->cc_number($cc_payload->{card}{last4}); + } + $payment->approval_code($approval_code) if $approval_code; $payment->cc_order_number($cc_order_number) if $cc_order_number; $payment->cc_type($cc_type) if $cc_type; 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 ddd8806f6d..ac11d61fae 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Account.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Account.pm @@ -1329,7 +1329,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 + billing_zip stripe_token /); my $cache_args = { diff --git a/Open-ILS/src/templates/opac/myopac/main_pay.tt2 b/Open-ILS/src/templates/opac/myopac/main_pay.tt2 index 9352b45e9e..e96f833695 100644 --- a/Open-ILS/src/templates/opac/myopac/main_pay.tt2 +++ b/Open-ILS/src/templates/opac/myopac/main_pay.tt2 @@ -11,7 +11,8 @@ [% ctx.payment_response.desc || ctx.payment_response.textcode %]
[% ctx.payment_response.note %] - [% ctx.payment_response.payload.error_message %] + [% ctx.payment_response.payload.error_message | html %] + [% ctx.payment_response.payload.message | html %]

[% 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 7d4740d796..9ed377d67a 100644 --- a/Open-ILS/src/templates/opac/myopac/main_payment_form.tt2 +++ b/Open-ILS/src/templates/opac/myopac/main_payment_form.tt2 @@ -5,7 +5,11 @@ myopac_main_page = "payment_form"; last_chance = CGI.param("last_chance"); -%] + + IF myopac_main_page == "payment_form" AND + ctx.get_org_setting(ctx.user.home_ou.id, 'credit.processor.stripe.enabled') AND ctx.get_org_setting(ctx.user.home_ou.id, 'credit.processor.default') == 'Stripe'; + ctx.use_stripe = 1; + END %]

[% l('Pay Fines') %]

[% IF ctx.fines.balance_owed <= 0 %]
@@ -13,7 +17,14 @@ "total is non-positive. We cannot process non-positive amounts.") %]
[% ELSE %] -
+[% IF ctx.use_stripe %] + +[% END %] +
[% IF last_chance %]

[% l("Are you sure you are ready to charge [_1] to your credit card?", money(ctx.fines.balance_owed)) %]

@@ -26,7 +37,7 @@ [% l('Cancel') %] [% ELSE %] @@ -37,7 +48,7 @@ [% FOR xact IN CGI.param('xact_misc') %] [% END %] - [% IF use_stripe %] + [% IF ctx.use_stripe %] [% END %] @@ -135,8 +146,8 @@ - - [% year = date.format(date.now, '%Y'); y = year; diff --git a/Open-ILS/src/templates/opac/parts/base.tt2 b/Open-ILS/src/templates/opac/parts/base.tt2 index e596af085e..251397335b 100644 --- a/Open-ILS/src/templates/opac/parts/base.tt2 +++ b/Open-ILS/src/templates/opac/parts/base.tt2 @@ -19,9 +19,9 @@ @import "[% ctx.media_prefix %]/js/dojo/dijit/themes/tundra/tundra.css"; [% END %] - [% INCLUDE 'opac/parts/goog_analytics.tt2' %] - [% INCLUDE 'opac/parts/stripe.tt2' %] + [% INCLUDE 'opac/parts/goog_analytics.tt2' %] + [% PROCESS 'opac/parts/stripe.tt2' %]

[% l('Catalog') %]

diff --git a/Open-ILS/src/templates/opac/parts/js.tt2 b/Open-ILS/src/templates/opac/parts/js.tt2 index 67d09fb263..ed68480402 100644 --- a/Open-ILS/src/templates/opac/parts/js.tt2 +++ b/Open-ILS/src/templates/opac/parts/js.tt2 @@ -1,6 +1,10 @@ +[%- IF ctx.use_stripe %] + +[% END -%] + [%- IF ctx.is_staff %] [% IF ctx.page == 'record' %] diff --git a/Open-ILS/src/templates/opac/parts/stripe.tt2 b/Open-ILS/src/templates/opac/parts/stripe.tt2 index feeba12746..ea4ca04a61 100644 --- a/Open-ILS/src/templates/opac/parts/stripe.tt2 +++ b/Open-ILS/src/templates/opac/parts/stripe.tt2 @@ -1,15 +1,12 @@ -[%- PROCESS "opac/parts/header.tt2"; -IF myopac_main_page == "payment_form" AND -ctx.get_org_setting(ctx.user.home_ou.id, 'credit.processor.stripe.enabled') AND ctx.get_org_setting(ctx.user.home_ou.id, 'credit.processor.default') == 'Stripe'; - use_stripe = 1; %] - +[%- PROCESS "opac/parts/header.tt2" %] +[% IF ctx.use_stripe %]