Support Stripe payments with some new code and some rearranged code ...
authorLebbeous Fogle-Weekley <lebbeous@esilibrary.com>
Thu, 19 Sep 2013 15:00:00 +0000 (11:00 -0400)
committerDan Wells <dbw2@calvin.edu>
Thu, 20 Feb 2014 21:31:38 +0000 (16:31 -0500)
... 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 <lebbeous@esilibrary.com>
Conflicts:
Open-ILS/src/templates/opac/myopac/main_payment_form.tt2

Signed-off-by: Remington Steed <rjs7@calvin.edu>
Signed-off-by: Dan Wells <dbw2@calvin.edu>
13 files changed:
Open-ILS/src/extras/install/Makefile.debian-squeeze
Open-ILS/src/extras/install/Makefile.debian-wheezy
Open-ILS/src/extras/install/Makefile.fedora
Open-ILS/src/extras/install/Makefile.ubuntu-lucid
Open-ILS/src/extras/install/Makefile.ubuntu-precise
Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/CreditCard.pm
Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Money.pm
Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Account.pm
Open-ILS/src/templates/opac/myopac/main_pay.tt2
Open-ILS/src/templates/opac/myopac/main_payment_form.tt2
Open-ILS/src/templates/opac/parts/base.tt2
Open-ILS/src/templates/opac/parts/js.tt2
Open-ILS/src/templates/opac/parts/stripe.tt2

index 8439fde..69f9151 100644 (file)
@@ -67,6 +67,7 @@ export DEB_APACHE_DISMODS = \
 
 export CPAN_MODULES = \
        Business::OnlinePayment::PayPal \
+       Business::Stripe \
        Library::CallNumber::LC \
        Net::Z3950::Simple2ZOOM \
        RPC::XML \
index 755acd0..e4634d3 100644 (file)
@@ -70,6 +70,7 @@ export DEB_APACHE_DISMODS = \
 
 export CPAN_MODULES = \
        Business::OnlinePayment::PayPal \
+       Business::Stripe \
        Template::Plugin::POSIX \
        Safe
 
index d70e455..0149387 100644 (file)
@@ -64,6 +64,7 @@ export CPAN_MODULES = \
        Business::ISSN \
        Net::Z3950::ZOOM \
        Net::Z3950::Simple2ZOOM \
+       Business::Stripe \
        Template::Plugin::POSIX \
        SRU \
        Rose::URI
index 02829b3..5400bcc 100644 (file)
@@ -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 \
index b6f3243..2418a01 100644 (file)
@@ -72,6 +72,7 @@ export DEB_APACHE_DISMODS = \
 
 export CPAN_MODULES = \
        Business::CreditCard::Object \
+       Business::Stripe \
        Business::OnlinePayment::PayPal \
        Template::Plugin::POSIX \
        Rose::URI \
index 6ac63a7..2c61830 100644 (file)
@@ -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')
index a6220ed..be2c88b 100644 (file)
@@ -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;
index ddd8806..ac11d61 100644 (file)
@@ -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 = {
index 9352b45..e96f833 100644 (file)
@@ -11,7 +11,8 @@
                 [% ctx.payment_response.desc || ctx.payment_response.textcode %]
             </span><br />
             [% ctx.payment_response.note %]
-            [% ctx.payment_response.payload.error_message %]
+            [% ctx.payment_response.payload.error_message | html %]
+            [% ctx.payment_response.payload.message | html %]
         </div>
         <p>
             [%
index 7d4740d..9ed377d 100644 (file)
@@ -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 %]
 <h3 class="sr-only">[% l('Pay Fines') %]</h3>
 [% IF ctx.fines.balance_owed <= 0 %]
 <div>
         "total is non-positive.  We cannot process non-positive amounts.") %]
 </div>
 [% ELSE %]
-<div id="pay_fines_now">
+[% IF ctx.use_stripe %]
+<noscript>
+    [% l("Your browser does not have Javascript enabled, and we cannot " _
+        "process credit card payments without it.  Please change your " _
+        "browser settings and try again.") %]
+</noscript>
+[% END %]
+<div id="pay_fines_now"[% IF ctx.use_stripe %] class="hide_me"[% END %]>
     [% IF last_chance %]
     <p><big>[% l("Are you sure you are ready to charge [_1] to your credit card?", money(ctx.fines.balance_owed)) %]</big></p>
     <form action="[% ctx.opac_root %]/myopac/main_pay_init" method="post">
@@ -26,7 +37,7 @@
         <a href="[% mkurl(ctx.opac_root _ '/myopac/main', {}, 1) %]">[% l('Cancel') %]</a>
     [% ELSE %]
     <form method="post" id="payment_form"
-    [% IF use_stripe %]
+    [% IF ctx.use_stripe %]
     onsubmit="return stripe_onsubmit();"
     [% END %]
     >
@@ -37,7 +48,7 @@
         [% FOR xact IN CGI.param('xact_misc') %]
         <input type="hidden" name="xact_misc" value="[% xact | html %]" />
         [% END %]
-        [% IF use_stripe %]
+        [% IF ctx.use_stripe %]
         <input type="hidden" name="stripe_token" id="stripe_token" />
         [% END %]
 
                 </tr>
                 <tr>
                     <td><label for="payment-credit-card">[% l('Credit Card #') %]</label></td>
-                    <td><input type="number" maxlength="16" id="payment-credit-card"
-                    [% IF use_stripe %]
+                    <td><input type="text" maxlength="16" id="payment-credit-card"
+                    [% IF ctx.use_stripe %]
                     data-stripe="number"
                     [% ELSE %]
                     name="number"
                 <tr>
                     <td><label for="payment-security-code">[% l('Security Code') %]</label></td>
                     <td>
-                        <input type="number" size="4" maxlength="5" id="payment-security-code"
-                        [% IF use_stripe %]
+                        <input type="text" size="4" maxlength="5" id="payment-security-code"
+                        [% IF ctx.use_stripe %]
                         data-stripe="cvc"
                         [% ELSE %]
                         name="cvv2"
                     <td><label for="payment-expire-month">[% l('Expiration Month') %]</label></td>
                     <td>
                         <select id="payment-expire-month"
-                        [% IF use_stripe %]
+                        [% IF ctx.use_stripe %]
                         data-stripe="exp_month"
                         [% ELSE %]
                         name="expire_month"
                     <td><label for="payment-expire-year">[% l('Expiration Year') %]</label></td>
                     <td>
                         <select id="payment-expire-year"
-                        [% IF use_stripe %]
+                        [%- IF ctx.use_stripe %]
                         data-stripe="exp_year"
                         [% ELSE %]
                         name="expire_year"
-                        [% END %]
+                        [% END -%]
                         >
                         [% year = date.format(date.now, '%Y');
                         y = year;
index e596af0..2513973 100644 (file)
@@ -19,9 +19,9 @@
             @import "[% ctx.media_prefix %]/js/dojo/dijit/themes/tundra/tundra.css";
         </style>
         [% END %]
-        [% INCLUDE 'opac/parts/goog_analytics.tt2' %]
-        [% INCLUDE 'opac/parts/stripe.tt2' %]
 
+        [% INCLUDE 'opac/parts/goog_analytics.tt2' %]
+        [% PROCESS 'opac/parts/stripe.tt2' %]
     </head>
     <body[% IF want_dojo; ' class="tundra"'; END %]>
         <h1 class="sr-only">[% l('Catalog') %]</h1>
index 67d09fb..ed68480 100644 (file)
@@ -1,6 +1,10 @@
 <!-- JS imports, etc.  -->
 <script type="text/javascript" src="[% ctx.media_prefix %]/js/ui/default/opac/simple.js"></script>
 
+[%- IF ctx.use_stripe %]
+<script type="text/javascript">unHideMe($("pay_fines_now"));[%# the DOM is loaded now, right? %]</script>
+[% END -%]
+
 [%- IF ctx.is_staff %]
 <script type="text/javascript" src="[% ctx.media_prefix %]/js/ui/default/opac/staff.js"></script>
     [% IF ctx.page == 'record' %]
index feeba12..ea4ca04 100644 (file)
@@ -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 %]
         <script type="text/javascript" src="https://js.stripe.com/v2/"></script> <!-- use an ou setting for this url? -->
         <script type="text/javascript">
         // This script is only displayed when logged in, so ctx.user.home_ou is always available
         Stripe.setPublishableKey('[% ctx.get_org_setting(ctx.user.home_ou.id, 'credit.processor.stripe.pubkey') %]');
 
         function stripe_onsubmit() {
-            var form = document.getElementById("stripe_form");
+            var form = document.getElementById("payment_form");
             var button = document.getElementById("payment_submit");
 
             button.disabled = true;