JBAS-1925 Ecard (Quipu) data and web form
authorBill Erickson <berickxx@gmail.com>
Wed, 3 Jan 2018 22:29:48 +0000 (17:29 -0500)
committerBill Erickson <berickxx@gmail.com>
Thu, 21 Mar 2019 19:46:23 +0000 (15:46 -0400)
* Ecard user groups, policies, and barcode generator.
* TPAC-driven web form and submit API (/eg/opac/ecard/form)
* TPAC-driven card verification form (/eg/opac/ecard/verify)
* API / form Test script

Signed-off-by: Bill Erickson <berickxx@gmail.com>
17 files changed:
KCLS/openils/var/templates_kcls/opac/ecard/form.tt2 [new file with mode: 0644]
KCLS/openils/var/templates_kcls/opac/ecard/submit.tt2 [new file with mode: 0644]
KCLS/openils/var/templates_kcls/opac/ecard/verify.tt2 [new file with mode: 0644]
KCLS/openils/var/templates_kcls/opac/parts/googalytics_new.tt2 [new file with mode: 0644]
KCLS/openils/var/templates_kcls/opac/register.tt2
KCLS/sql/schema/deploy/ecard-data.sql [new file with mode: 0644]
KCLS/sql/schema/revert/ecard-data.sql [new file with mode: 0644]
KCLS/sql/schema/sqitch.plan
KCLS/sql/schema/verify/ecard-data.sql [new file with mode: 0644]
KCLS/test-scripts/ecard/test-submit.pl [new file with mode: 0755]
KCLS/utility-scripts/CRONTAB
KCLS/utility-scripts/overdue/run_holds.sh
KCLS/utility-scripts/patron_activity/patron_activity.sql
KCLS/utility-scripts/purge_ecards/purge_ecards.sh [new file with mode: 0755]
Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader.pm
Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Ecard.pm [new file with mode: 0644]
Open-ILS/web/js/ui/default/actor/user/register.js

diff --git a/KCLS/openils/var/templates_kcls/opac/ecard/form.tt2 b/KCLS/openils/var/templates_kcls/opac/ecard/form.tt2
new file mode 100644 (file)
index 0000000..3ac1a84
--- /dev/null
@@ -0,0 +1,125 @@
+<!doctype html>
+<html>
+  <head>
+    <meta charset="utf-8">
+    <title>[% l('Get an eCard') %]</title>
+    [% INCLUDE 'opac/parts/googalytics_new.tt2' %]
+
+    <!-- NOTE: BootstrapCSS v4 does not play nicely with BC headers -->
+    <link rel="stylesheet" href="/js/ui/default/staff/build/css/bootstrap.min.css" />
+
+    <!-- QUIPU CSS -->
+    <link rel="stylesheet" href="https://ecard.quipugroup.net/css/eCARD.css">
+    <link rel="stylesheet"
+      href="https://ecard.quipugroup.net/js/jqueryUI/css/ui-lightness/jquery-ui-1.10.4.custom.min.css">
+    <!-- END QUIPU CSS -->
+
+    <!-- local CSS -->
+    <style>
+      /* BC screen reader links are not correctly hidden via their API.
+         Add some additional CSS to hide them */
+      .screen_reader_nav {
+        position: absolute;
+        top: -1000px;
+        left: -1000px;
+        z-index: 0;
+      }
+      #ecard-container-wrapper {
+        width: 98%;
+      }
+      #ecard-container {
+        margin-bottom: 20px;
+        color: #585d5e;
+        font-family: 'Open Sans', sans-serif;
+        letter-spacing: .5pt;
+        font-size: 15px;
+        width: 900px; /* to match bibliocms */
+        margin-left: auto;
+        margin-right: auto;
+      }
+    </style>
+
+    <!-- BC CSS -->
+    [% ctx.bc_css %]
+    <!-- BC END CSS -->
+
+  </head>
+  <body>
+
+    <!-- BC SCREEN READER NAVIGATION -->
+    [% ctx.bc_screen_reader_navigation %]
+    <!-- BC END SCREEN READER NAVIGATION -->
+
+    <!-- BC HEADER -->
+    [% ctx.bc_header %]
+    <!-- BC END HEADER -->
+
+    <div id='ecard-container-wrapper'>
+      <div id='ecard-container'>
+
+        <div id='ecard-preamble'>
+
+          <h1>Get a KCLS eCard</h1>
+
+          <p>
+            Please fill out the application below to get immediate 24/7
+            access to King County Library System’s online services
+            including e-books and audiobooks, movies and music, online
+            classes, exam prep and research databases, and magazines.
+          </p>
+          <p>
+            Please apply <a title="Get A Library Card"
+              href="https://w3.kcls.org/get-a-library-card">here</a>
+            if you would prefer a card with full library privileges 
+            including check out, computer use and printing.
+          </p>
+          <p>
+            If your eCard application is successful we will mail
+            a confirmation letter to you in order to verify your
+            address. You must follow the instructions in the letter so
+            that the account remains open.
+          </p>
+          <p>
+            Want more information about the eCard and who qualifies to
+            use it? Read the FAQ.
+          </p>
+        </div>
+
+        <div id="eCARD" data-language="en" data-branchid="">
+          <!-- eCARD requires JavaScript in order to display the registration form -->
+          <!-- The following will detect if JavaScript is enabled on the patron's browser -->
+          <noscript>
+            <h2 style="color:red;">Warning - JavaScript Required</h2>
+            <p>
+             For full functionality of this web page it is necessary to enable 
+             JavaScript in your browser. For more information on most browsers, try 
+             <a href="http://www.enable-javascript.com/" target="_blank">How to enable JavaScript</a> 
+             OR <a href="http://activatejavascript.org/en/instructions" target="_blank">activatejavascript.org</a>
+            </p>
+          </noscript>
+        </div>
+      </div>
+    </div>
+
+    <!-- BC FOOTER -->
+    [% ctx.bc_footer %]
+    <!-- BC END FOOTER -->
+
+
+    <!-- QUIPU JS -->
+    <script type="text/javascript" src="https://ecard.quipugroup.net/js/jquery-1.11.1.min.js"></script>
+    <script type="text/javascript" src="https://ecard.quipugroup.net/js/jqueryUI/js/jquery-ui-1.10.4.custom.min.js"></script>
+    <script type="text/javascript" src="https://ecard.quipugroup.net/js/jquery.xdomainrequest.min.js"></script>
+    <script type="text/javascript" src="https://ecard.quipugroup.net/js/jquery.PrintArea.js"></script>
+    <script type="text/javascript" src="https://ecard.quipugroup.net/js/eCARDMain.js"></script>
+    <script type="text/javascript" src="https://ecard.quipugroup.net/Libraries/22/eCARDLibrary.js"></script>
+    <!-- END QUIPU JS -->
+
+    <!-- BC requires jquery, loaded from quipu (above) in this form -->
+    <!-- BC JS -->
+    [% ctx.bc_js %]
+    <!-- BC END JS -->
+
+  </body>
+</html>
+
diff --git a/KCLS/openils/var/templates_kcls/opac/ecard/submit.tt2 b/KCLS/openils/var/templates_kcls/opac/ecard/submit.tt2
new file mode 100644 (file)
index 0000000..ed3e58d
--- /dev/null
@@ -0,0 +1 @@
+[% ctx.response %]
diff --git a/KCLS/openils/var/templates_kcls/opac/ecard/verify.tt2 b/KCLS/openils/var/templates_kcls/opac/ecard/verify.tt2
new file mode 100644 (file)
index 0000000..ee924d3
--- /dev/null
@@ -0,0 +1,153 @@
+<!doctype html>
+<html>
+  <head>
+    <meta charset="utf-8">
+    <title>[% l('Confirm an eCard Account') %]</title>
+    [% INCLUDE 'opac/parts/googalytics_new.tt2' %]
+
+    <link rel="stylesheet" href="/js/ui/default/staff/build/css/bootstrap.min.css" />
+
+    <!-- local CSS -->
+    <style>
+      /* BC screen reader links are not correctly hidden via their API.
+         Add some additional CSS to hide them */
+      .screen_reader_nav {
+        position: absolute;
+        top: -1000px;
+        left: -1000px;
+        z-index: 0;
+      }
+      #ecard-container-wrapper {
+        width: 98%;
+      }
+      #ecard-container {
+        margin-bottom: 20px;
+        color: #585d5e;
+        font-family: 'Open Sans', sans-serif;
+        letter-spacing: .5pt;
+        font-size: 15px;
+        width: 900px; /* to match bibliocms */
+        margin-left: auto;
+        margin-right: auto;
+      }
+    </style>
+
+    <script>
+      function handleSubmit() {
+        if (typeof ga === 'function') {
+          ga('send', 'event', 'Ecard Verify', 'submit', 'Forms');
+        }
+        return true;
+      }
+
+      function handleCancel() {
+        // unused at time of writing, keep around just in case.
+        if (typeof ga === 'function') {
+          ga('send', 'event', 'Ecard Verify', 'cancel', 'Forms');
+        }
+        return false; // avoid submit
+      }
+    </script>
+
+    <!-- BC CSS -->
+    [% ctx.bc_css %]
+    <!-- BC END CSS -->
+
+  </head>
+  <body>
+
+    <!-- BC SCREEN READER NAVIGATION -->
+    [% ctx.bc_screen_reader_navigation %]
+    <!-- BC END SCREEN READER NAVIGATION -->
+
+    <!-- BC HEADER -->
+    [% ctx.bc_header %]
+    <!-- BC END HEADER -->
+
+    <div id='ecard-container-wrapper'>
+      <div id='ecard-container'>
+
+        <div id='ecard-preamble'>
+          <h1>Verify Your KCLS eCard</h1>
+          <p>
+            Please fill out the form below to verify your eCard account.
+          </p>
+          [% IF ctx.verify_failed %]
+          <div id='ecard-verify-failed'>
+            <div class="alert alert-warning" role="alert">
+              <p>
+                It appears that your confirmation did not go through.
+                This could happen for a variety of reasons:
+              </p>
+              <br/>
+              <ul>
+                <li>You mistyped the confirmation code</li>
+                <li>
+                  You typed in the code, but you provided it more than 30
+                  days after your application. If so, please feel free to reapply.
+                </li>
+                <li>You already confirmed, so your eCard is ready to use!</li>
+              </ul>
+              <br/>
+              <p>
+                If you have any questions about how to confirm your eCard
+                please contact <a href="https://kcls.org/ask/">Ask KCLS</a>
+                or staff at <a href="https://kcls.bibliocommons.com/locations">
+                your neighborhood library</a>.
+              </p>
+            </div>
+          </div>
+          [% END %]
+        </div>
+
+        [% IF ctx.verify_success %]
+        <div id='ecard-verify-success'>
+          <div class="alert alert-success" role="alert">
+            <p>
+            Congratulations! You have successfully confirmed your KCLS eCard!
+            </p>
+            <p>
+            <b>If you have not yet had a chance to find out what you can do with
+            your eCard, go to <a href="https://kcls.org/onlinelibrary">KCLS Online Resources</a>.
+            Read, stream, listen, find information, advance your studies, and enjoy!
+            </p>
+          </div>
+        </div>
+        [% ELSE %]
+        <div id='ecard-verify-form' class='col-md-6'>
+          <form method='POST' onsubmit="return handleSubmit()">
+            <div class="form-group">
+              <label for="barcode">Barcode</label>
+              <input type="text" class="form-control" id="barcode"
+                name="barcode" placeholder="Barcode"
+                value="[% ctx.barcode | html %]"/>
+            </div>
+            <div class="form-group">
+              <label for="verification_code">6-Character Verification Code</label>
+              <input type="text" class="form-control" id="verification_code"
+                name="verification_code" placeholder="Verification Code"
+                value="[% ctx.verify_code | html %]"/>
+            </div>
+            <button type="submit" class="btn btn-default">Submit</button>
+          </form>
+        </div>
+        <div style="clear:both"></div>
+        [% END %]
+
+      </div>
+    </div>
+
+    <!-- BC FOOTER -->
+    [% ctx.bc_footer %]
+    <!-- BC END FOOTER -->
+
+    <!-- unlike the quipu form page, we have to manaully load jquery here -->
+    <script src="[% ctx.media_prefix %]/js/ui/default/common/build/js/jquery.min.js"></script>
+
+    <!-- BC JS -->
+    [% ctx.bc_js %]
+    <!-- BC END JS -->
+
+  </body>
+</html>
+
diff --git a/KCLS/openils/var/templates_kcls/opac/parts/googalytics_new.tt2 b/KCLS/openils/var/templates_kcls/opac/parts/googalytics_new.tt2
new file mode 100644 (file)
index 0000000..621c6e7
--- /dev/null
@@ -0,0 +1,10 @@
+<script type="text/javascript">
+  (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
+  (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
+  m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
+  })(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
+
+  ga('create', 'UA-3018520-10', 'auto');
+  ga('send', 'pageview');
+</script>
+
index f9d5f60..7b2a810 100644 (file)
@@ -1,7 +1,7 @@
 [%- PROCESS "opac/parts/header.tt2";
     PROCESS "opac/parts/org_selector.tt2";
     PROCESS "opac/parts/state_selector.tt2";
-    ctx.page_title = l("Get a Library Card");
+    ctx.page_title = l("Get an All-Access Library Card");
 
 # for privacy, reload the page after (default) 5 minutes
 return_to  = CGI.param('return-to');
@@ -82,16 +82,7 @@ END; # input_field()
     <title>[% l('[_1]', ctx.page_title) %]</title>
 
     <!-- testing alternate googalytics invocation -->
-    [%# INCLUDE 'opac/parts/goog_analytics.tt2' %]
-    <script>
-      (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
-      (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
-      m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
-      })(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
-
-      ga('create', 'UA-3018520-10', 'auto');
-      ga('send', 'pageview');
-    </script>
+    [% INCLUDE 'opac/parts/googalytics_new.tt2' %]
 
     <script src="[% ctx.media_prefix %]/js/ui/default/common/build/js/jquery.min.js"></script>
     <script type="text/javascript"
diff --git a/KCLS/sql/schema/deploy/ecard-data.sql b/KCLS/sql/schema/deploy/ecard-data.sql
new file mode 100644 (file)
index 0000000..82eb9f6
--- /dev/null
@@ -0,0 +1,210 @@
+-- Deploy kcls-evergreen:ecard-data to pg
+
+BEGIN;
+
+
+-- Start at 100 to avoid barcodes with long stretches of zeros early on.
+-- eCard barcodes have 7 auto-generated digits.
+CREATE SEQUENCE actor.auto_barcode_ecard_seq START 100 MAXVALUE 9999999;
+
+CREATE OR REPLACE FUNCTION actor.generate_barcode
+    (prefix TEXT, numchars INTEGER, seqname TEXT) RETURNS TEXT AS
+$FUNK$
+/* 
+Generate a barcode starting with 'prefix' and followed by 'numchars'
+numbers.  The auto portion numbers are generated from the provided
+sequence, guaranteeing uniquness across all barcodes generated with
+the same sequence.  The number is left-padded with zeros to meet the
+numchars size requirement.  Returns NULL if the sequnce value is 
+higher than numchars can accommodate .*/
+    SELECT NEXTVAL($3); -- bump the sequence up 1
+    SELECT CASE                                                                
+        WHEN LENGTH(CURRVAL($3)::TEXT) > $2 THEN NULL                          
+        ELSE $1 || LPAD(CURRVAL($3)::TEXT, $2, '0')                            
+    END; 
+$FUNK$ LANGUAGE SQL;
+
+
+CREATE OR REPLACE FUNCTION actor.purge_expired_users
+    (profile_grp INTEGER, expired_for INTERVAL) RETURNS INTEGER AS
+$FUNK$
+DECLARE
+    purge_count INTEGER DEFAULT 0;
+    user_id INTEGER;
+BEGIN
+    FOR user_id IN 
+        SELECT id FROM actor.usr
+        WHERE profile = profile_grp AND NOT deleted
+            -- Never purge active users (possible w/ negative intervals)
+            AND expire_date < NOW()
+            AND (NOW() - expire_date) > expired_for 
+    LOOP
+
+        RAISE NOTICE 'Purging user %', user_id;
+        PERFORM actor.usr_delete(user_id, NULL::INTEGER);
+        purge_count := purge_count + 1;
+
+    END LOOP;
+    RETURN purge_count;
+END;
+$FUNK$ LANGUAGE PLPGSQL;
+
+DO $$
+BEGIN
+    IF evergreen.insert_on_deploy() THEN
+
+-- no one gets permission to edit this account
+INSERT INTO permission.perm_list (code, description) VALUES
+    ('group_application.user.ecard', 'Ecard Patrons'),
+    ('group_application.vendor', 'View/modify vendor login accounts');
+
+-- TODO: merge lost/paid receipts vendor profile changes!!
+
+INSERT INTO permission.grp_tree
+    (id, name, parent, usergroup, perm_interval, description, application_perm)
+VALUES
+    (   920, 'Vendor', 1, FALSE, '2 years', 
+        'Minimal-access vendor logins',
+        'group_application.vendor' 
+    ), 
+    (   921, 'Ecard Vendor', 920, FALSE, '2 years',
+        'Ecard vendor API account',
+        NULL
+    ),
+    (   950, 'Ecard Groups', 1, FALSE, '2 years',
+        'Parent group of all ECARD groups',
+        'goup_application.user.ecard'
+    ),
+    (   951, 'Temporary Ecard', 950, TRUE, '1 month',
+        'Provisional Ecard Accounts.  Verification required',
+        NULL
+    ),
+    (   952, 'Ecard', 950, TRUE, '2 years',
+        'Standard access Ecard account',
+        NULL
+    )
+;
+
+INSERT INTO permission.grp_perm_map (grp, perm, depth) VALUES
+    (951, (SELECT id FROM permission.perm_list
+        WHERE code = 'ACCESS_EBOOKS_AND_DATABASES'), 0),
+    (952, (SELECT id FROM permission.perm_list
+        WHERE code = 'ACCESS_EBOOKS_AND_DATABASES'), 0),
+    (951, 2, 0), -- OPAC_LOGIN
+    (952, 2, 0); -- OPAC_LOGIN
+
+-- Add the goup_application.user.ecard permission to all users
+-- that already have the 'group_application.user.patron' perm.
+INSERT INTO permission.grp_perm_map (grp, perm, depth)
+    SELECT map.grp, perm.id, 0
+    FROM permission.grp_perm_map map, permission.perm_list perm
+    WHERE map.perm = 126 -- group_application.user.patron
+    AND perm.code = 'goup_application.user.ecard';
+
+-- extra prevention for ecard checkouts
+INSERT INTO config.circ_matrix_matchpoint
+    (active, org_unit, grp, circulate) VALUES (TRUE, 1, 950, FALSE);
+
+-- hold matchpoints have to match the group exactly to overcome
+-- conflicting match weights.
+INSERT INTO config.hold_matrix_matchpoint (
+    active, usr_grp, requestor_grp, ref_flag, holdable,
+    distance_is_from_owner, include_frozen_holds, stop_blocked_user,
+    strict_ou_match
+) VALUES (TRUE, 951, 1, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE);
+
+INSERT INTO config.hold_matrix_matchpoint (
+    active, usr_grp, requestor_grp, ref_flag, holdable,
+    distance_is_from_owner, include_frozen_holds, stop_blocked_user,
+    strict_ou_match
+) VALUES (TRUE, 952, 1, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE);
+
+-- new ident type for students
+INSERT INTO config.identification_type (id, name)
+    VALUES (102, 'Ecard confirmation code');
+
+PERFORM SETVAL('config.identification_type_id_seq', 102);
+
+INSERT INTO actor.passwd_type (code, name, login, crypt_algo, iter_count)
+    VALUES ('ecard_vendor', 'Ecard Vendor Secret', FALSE, 'bf', 8);
+
+INSERT INTO actor.usr (
+  profile, usrname, passwd, ident_type, first_given_name,
+  family_name, home_ou, alert_message, net_access_level)
+VALUES (
+    921, -- ecard vendor
+    'quipu',
+    RANDOM()::TEXT, -- leave main login password unguessable
+    3, -- other
+    'Quipu',
+    'EcardVendor',
+    1,
+    'Ecard Vendor Account. Do Not Modify',
+    101
+);
+
+-- start with a random password, manually change later.
+PERFORM actor.set_passwd(
+    (SELECT id FROM actor.usr WHERE usrname = 'quipu' AND profile = 921),
+    'ecard_vendor',
+    RANDOM()::TEXT
+);
+
+
+INSERT INTO action_trigger.hook (key, core_type, description, passive) 
+    VALUES ('au.create.ecard', 'au', 'New eCard user created', FALSE);
+
+INSERT INTO action_trigger.event_definition (active, owner, name, hook, 
+    validator, reactor, granularity, retention_interval, template)
+VALUES (
+    TRUE,
+    1,
+    'eCard Verification Postcard',
+    'au.create.ecard',
+    'NOOP_True',
+    'ProcessTemplate',
+    'Daily-Active-Print',
+    '1 month',
+$TEMPLATE$
+[%- USE date -%]
+[%- SET user = target -%]
+[%- SET user_addr = user.billing_address -%]
+<notice type='ecard'>
+    <patron>
+        <barcode>[% helpers.escape_xml(user.card.barcode) %]</barcode>
+        <verification_code>[% helpers.escape_xml(user.ident_value) %]</verification_code>
+        <first_given_name>[% helpers.escape_xml(user.first_given_name) %]</first_given_name>
+        <family_name>[% helpers.escape_xml(user.family_name) %]</family_name>
+        <addr_valid>true</addr_valid>
+        <addr_street1>[% helpers.escape_xml(user_addr.street1) %]</addr_street1>
+        <addr_street2>[% helpers.escape_xml(user_addr.street2) %]</addr_street2>
+        <addr_city>[% helpers.escape_xml(user_addr.city) %]</addr_city>
+        <addr_state>[% helpers.escape_xml(user_addr.state) %]</addr_state>
+        <addr_post_code>[% helpers.escape_xml(user_addr.post_code) %]</addr_post_code>
+        <email>[% helpers.escape_xml(user.email) %]</email>
+        <day_phone>[% user.day_phone %]</day_phone>
+        <evening_phone>[% user.evening_phone %]</evening_phone>
+        <other_phone>[% user.other_phone %]</other_phone>
+        <sys_id>[% user.id %]</sys_id>
+    </patron>
+</notice>
+$TEMPLATE$
+);
+
+INSERT INTO action_trigger.environment (event_def, path) VALUES 
+(
+    (SELECT id FROM action_trigger.event_definition 
+        WHERE hook = 'au.create.ecard'), 
+    'billing_address'
+), (
+    (SELECT id FROM action_trigger.event_definition 
+        WHERE hook = 'au.create.ecard'), 
+    'card'
+);
+
+
+    END IF; -- INSERT ON DEPLOY
+END $$;
+
+COMMIT;
+
diff --git a/KCLS/sql/schema/revert/ecard-data.sql b/KCLS/sql/schema/revert/ecard-data.sql
new file mode 100644 (file)
index 0000000..f89786f
--- /dev/null
@@ -0,0 +1,42 @@
+-- Revert kcls-evergreen:ecard-data from pg
+
+BEGIN;
+
+DROP FUNCTION actor.purge_expired_users (INTEGER, INTERVAL);
+
+DELETE FROM action_trigger.environment WHERE event_def = 
+    (SELECT id FROM action_trigger.event_definition 
+        WHERE hook = 'au.create.ecard'); 
+DELETE FROM action_trigger.event_definition WHERE hook = 'au.create.ecard';
+DELETE FROM action_trigger.hook WHERE key = 'au.create.ecard';
+
+DELETE FROM actor.passwd WHERE passwd_type = 'ecard_vendor';
+DELETE FROM actor.passwd_type WHERE code = 'ecard_vendor';
+
+SELECT actor.usr_delete(
+    (SELECT id FROM actor.usr WHERE usrname = 'quipu' AND profile = 921), NULL);
+
+DELETE FROM config.identification_type WHERE id = 102; -- ecard verification
+
+DELETE FROM config.hold_matrix_matchpoint WHERE usr_grp IN (951, 952);
+
+DELETE FROM config.circ_matrix_matchpoint WHERE grp = 950;
+
+DELETE FROM permission.grp_perm_map WHERE perm = (
+    SELECT id FROM permission.perm_list 
+    WHERE code = 'goup_application.user.ecard'
+);
+
+DELETE FROM permission.grp_perm_map WHERE grp IN (920, 921, 950, 951, 952);
+
+DELETE FROM permission.grp_tree WHERE id IN (920, 921, 950, 951, 952);
+
+DELETE FROM permission.perm_list WHERE code = 'group_application.user.ecard';
+DELETE FROM permission.perm_list WHERE code = 'group_application.vendor';
+
+
+DROP FUNCTION actor.generate_barcode (TEXT, INTEGER, TEXT);
+
+DROP SEQUENCE actor.auto_barcode_ecard_seq;
+
+COMMIT;
index eed4779..9e9795d 100644 (file)
@@ -69,3 +69,4 @@ vand-preserve-all-fields [vand-import-edit-date-fix] 2018-04-09T20:15:27Z Bill E
 track-bib-merges [vand-import-edit-date-fix] 2018-04-03T15:04:00Z Bill Erickson,,, <berick@kcls-dev-local> # Track bib merges
 search-index-keep-periods [vand-import-edit-date-fix] 2018-04-02T19:10:31Z Bill Erickson,,, <berick@kcls-dev-local> # Avoid stripping periods in search indexes
 acq-inv-close-fields [search-index-keep-periods] 2018-04-12T19:03:29Z Bill Erickson,,, <berick@kcls-dev-local> # Invoice export by/date fields
+ecard-data [2.10-to-2.12-upgrade] 2018-01-03T21:55:03Z Bill Erickson,,, <berick@kcls-dev-local> # Ecard lib settings, policy data, etc.
diff --git a/KCLS/sql/schema/verify/ecard-data.sql b/KCLS/sql/schema/verify/ecard-data.sql
new file mode 100644 (file)
index 0000000..0ad4363
--- /dev/null
@@ -0,0 +1,7 @@
+-- Verify kcls-evergreen:ecard-data on pg
+
+BEGIN;
+
+-- XXX Add verifications here.
+
+ROLLBACK;
diff --git a/KCLS/test-scripts/ecard/test-submit.pl b/KCLS/test-scripts/ecard/test-submit.pl
new file mode 100755 (executable)
index 0000000..a7a73d8
--- /dev/null
@@ -0,0 +1,73 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+use LWP::UserAgent;
+use Data::Dumper;
+$Data::Dumper::Indent = 0;
+
+my $host = $ARGV[0] || 'localhost';
+my $url = "https://$host/eg/opac/ecard/submit";
+my $ua = LWP::UserAgent->new;
+my $vendor_user = 'quipu';
+my $vendor_pass = 'demo123';
+
+
+# XXX local test server has dummy cert
+$ua->ssl_opts(verify_hostname => 0, SSL_verify_mode => 0);
+
+my @tests = (
+    {
+        testmode => 'CONNECT'
+    }, {
+        vendor_username => $vendor_user,
+        vendor_password => $vendor_pass,
+        testmode => 'AUTH'
+    }, {
+        vendor_username => $vendor_user,
+        vendor_password => $vendor_pass,
+        testmode => 'API'
+    }, {
+        vendor_username => $vendor_user,
+        vendor_password => $vendor_pass,
+        datamode => 'org_units'
+    }, {
+        vendor_username => $vendor_user,
+        vendor_password => $vendor_pass,
+        first_given_name => 'Julius'
+    }, {
+        vendor_username => $vendor_user,
+        vendor_password => $vendor_pass,
+        first_given_name => 'Julius',
+        family_name => 'Cæsar', # test utf-8
+        email => 'IHateDaggers@example.org',
+        dob => '1998-07-13',
+        day_phone => '555-123-4567',
+        home_ou => 1492,
+        passwd => 'hailme',
+        billing_street1 => '123 Pineapple Lane',
+        billing_street1_name => 'Pineapplez', # bypass dupe checker
+        billing_street2 => 'Apt 23',
+        billing_city => 'Century City',
+        billing_post_code => '12346',
+        billing_county => 'Banana',
+        billing_state => 'WA',
+        billing_country => 'USA',
+        pickup_auth => 'Slim Jim',
+        events_mailing => 1,
+        foundation_mailing => undef
+    }
+);
+
+for my $test (@tests) {
+    print "Sending: " . Dumper($test) . "\n";
+    my $res = $ua->post($url, $test);
+
+    if ($res->is_success) {
+        print "SUCCESS response: " . $res->content;
+    } else {
+        print "ERROR response: " . $res->status_line;
+    }
+
+    print "\n";
+}
+
index 77e73dd..87121ae 100644 (file)
@@ -163,6 +163,10 @@ BACKSTAGE_PASSWORD = BSPASS
 # Nightly holds purging
 30 1 * * * cd $SCRIPT_DIR/purge_holds/ && ./purge_holds.sh
 
+# Nightly expired ecards purging
+10 2 * * * cd $SCRIPT_DIR/purge_ecards/ && ./purge_ecards.sh
+
+
 # Nightly Action/Trigger purging
 30 2 * * * cd $SCRIPT_DIR/purge_action_trigger/ && ./purge_action_trigger.sh
 
index 468ac5d..a7dcdea 100755 (executable)
@@ -5,9 +5,10 @@ EG_BIN=/openils/bin
 EG_CONF=/openils/conf
 AT_FILTERS=/openils/conf/a_t_filters/
 export OSRF_LOG_CLIENT=1
+ECARD_DEF_ID=162; # TODO: Apply prod ID when available.
 
-# Daily batched holds,collection print & phone
-$EG_BIN/action_trigger_runner.pl --osrf-config $SRF_CORE --run-pending --granularity Daily-Active-Print --granularity-only && ($EG_BIN/send-print-notices.pl --event-def 131 --prefix holds-available-print; $EG_BIN/send-print-notices.pl --event-def 132 --prefix collection;)
+# Daily batched holds,collection print & phone, ecard postcards
+$EG_BIN/action_trigger_runner.pl --osrf-config $SRF_CORE --run-pending --granularity Daily-Active-Print --granularity-only && ($EG_BIN/send-print-notices.pl --event-def 131 --prefix holds-available-print; $EG_BIN/send-print-notices.pl --event-def 132 --prefix collection; $EG_BIN/send-print-notices.pl --event-def $ECARD_DEF_ID --prefix ecard;)
 
 # Daily batched holds email (signed off: Bill).
 $EG_BIN/action_trigger_runner.pl --osrf-config $SRF_CORE --run-pending --granularity Daily-Hold --granularity-only
index 207d651..ee6551f 100644 (file)
@@ -11,6 +11,9 @@ UPDATE actor.usr usr
     ) 
     AND NOT barred
     AND NOT deleted
-    AND profile NOT IN (31, 91, 17, 26, 23, 32, 90, 92, 93, 901, 902, 903, 16);
+    AND profile NOT IN (
+        31, 91, 17, 26, 23, 32, 90, 92, 93, 901, 902, 903, 16, 
+        952 -- Provisional Ecard
+    );
 
 COMMIT;
diff --git a/KCLS/utility-scripts/purge_ecards/purge_ecards.sh b/KCLS/utility-scripts/purge_ecards/purge_ecards.sh
new file mode 100755 (executable)
index 0000000..167766f
--- /dev/null
@@ -0,0 +1,20 @@
+#!/bin/bash
+# -------------------------------------------------------------------------
+# Purge provisional eCard accounts that have been expired for at least 
+# one day.
+# Set PGHOST, PGPASSWORD, PGUSER environment variables!
+# -------------------------------------------------------------------------
+set -eu
+PSQL="psql"
+TIMEOUT="SET STATEMENT_TIMEOUT = 0"
+ECARD_PROFILE=952 # Provisional Ecard profile
+EXPIRED_FOR="1 day"
+
+echo -n "Purging expired eCard users at "
+date +"%F %T" 
+
+echo "$TIMEOUT; SELECT actor.purge_expired_users($ECARD_PROFILE, '$EXPIRED_FOR');" | $PSQL;
+
+echo -n "Purging expired eCard complete at "
+date +"%F %T" 
+
index dca6eb2..8491910 100644 (file)
@@ -26,6 +26,7 @@ use OpenILS::WWW::EGCatLoader::Record;
 use OpenILS::WWW::EGCatLoader::Container;
 use OpenILS::WWW::EGCatLoader::SMS;
 use OpenILS::WWW::EGCatLoader::Register;
+use OpenILS::WWW::EGCatLoader::Ecard;
 
 my $U = 'OpenILS::Application::AppUtils';
 
@@ -172,6 +173,10 @@ sub load {
 
     $self->load_simple("myopac") if $path =~ m:opac/myopac:; # A default page for myopac parts
 
+    return $self->load_ecard_form if $path =~ m|opac/ecard/form|;
+    return $self->load_ecard_submit if $path =~ m|opac/ecard/submit|;
+    return $self->load_ecard_verify if $path =~ m|opac/ecard/verify|;
+
     if($path =~ m|opac/login|) {
         return $self->load_login unless $self->editor->requestor; # already logged in?
 
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Ecard.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Ecard.pm
new file mode 100644 (file)
index 0000000..a53806b
--- /dev/null
@@ -0,0 +1,672 @@
+package OpenILS::WWW::EGCatLoader;
+use strict; use warnings;
+use Apache2::Const -compile => qw(OK FORBIDDEN HTTP_INTERNAL_SERVER_ERROR);
+use OpenSRF::Utils::Logger qw/$logger/;
+use OpenSRF::Utils::JSON;
+use OpenSRF::Utils qw/:datetime/;
+use OpenILS::Utils::Fieldmapper;
+use OpenILS::Application::AppUtils;
+use OpenILS::Utils::CStoreEditor qw/:funcs/;
+use OpenILS::Event;
+use Data::Dumper;
+use LWP::UserAgent;
+use OpenILS::Utils::KCLSNormalize;
+use DateTime;
+use Digest::MD5 qw(md5_hex);
+$Data::Dumper::Indent = 0;
+my $U = 'OpenILS::Application::AppUtils';
+
+
+my $PROVISIONAL_ECARD_GRP = 951;
+my $FULL_ECARD_GRP = 952;
+my $ECARD_VERIFY_IDENT = 102;
+
+my $HEADER_FOOTER_URL = 
+    'https://kcls.bibliocommons.com/widgets/external_templates.json';
+my $HEADER_FOOTER_TIMEOUT = 5;
+
+my @api_fields = (
+    {name => 'vendor_username', required => 1},
+    {name => 'vendor_password', required => 1},
+    {name => 'first_given_name', class => 'au', required => 1},
+    {name => 'second_given_name', class => 'au'},
+    {name => 'family_name', class => 'au', required => 1},
+    {name => 'email', class => 'au', required => 1},
+    {name => 'passwd', class => 'au', required => 1},
+    {name => 'day_phone', class => 'au', required => 1},
+    {name => 'dob', class => 'au', required => 1},
+    {name => 'home_ou', class => 'au', required => 1},
+    {name => 'ident_value2', 
+     class => 'au', 
+     notes => "AKA parent/guardian",
+     required_if => 'Patron is less than 18 years old'
+    },
+    {name => 'billing_street1', class => 'aua', required => 1},
+    {name => 'billing_street1_name'},
+    {name => 'billing_street2', class => 'aua'},
+    {name => 'billing_city', class => 'aua', required => 1},
+    {name => 'billing_post_code', class => 'aua', required => 1},
+    {name => 'billing_county', class => 'aua', required => 1},
+    {name => 'billing_state', class => 'aua', required => 1},
+    {name => 'billing_country', class => 'aua', required => 1},
+    {name => 'events_mailing', class => 'asc'},
+    {name => 'foundation_mailing', class => 'asc'}
+);
+
+# Random 6-character alpha-numeric code that avoids look-alike characters
+# https://ux.stackexchange.com/questions/53341/are-there-any-letters-numbers-that-should-be-avoided-in-an-id
+my @code_chars = ('A','C'..'F','H','J'..'N','P','R','T'..'Y','3','4','7','9');
+sub generate_verify_code {
+    my $string = '';
+    $string .= $code_chars[rand @code_chars] for 1..6;
+    return $string;
+}
+
+sub load_ecard_form {
+    my $self = shift;
+    my $ctx = $self->ctx;
+    my $cgi = $self->cgi;
+
+    $self->collect_header_footer;
+    return Apache2::Const::OK;
+}
+
+
+sub load_ecard_verify {
+    my $self = shift;
+    my $cgi = $self->cgi;
+    $self->collect_header_footer;
+
+    # Loading the form.
+    return Apache2::Const::OK if $cgi->request_method eq 'GET';
+
+    $self->verify_ecard;
+    return Apache2::Const::OK;
+}
+
+sub verify_ecard {
+    my $self = shift;
+    my $cgi = $self->cgi;
+    my $ctx = $self->ctx;
+    $self->log_params;
+
+    my $verify_code = $ctx->{verify_code} = $cgi->param('verification_code');
+    my $barcode = $ctx->{barcode} = $cgi->param('barcode');
+
+    $ctx->{verify_failed} = 1;
+
+    my $e = new_editor();
+
+    my $au = $e->search_actor_user({
+        profile => $PROVISIONAL_ECARD_GRP,
+        ident_type => $ECARD_VERIFY_IDENT,
+        ident_value => $verify_code
+    })->[0];
+
+    if (!$au) {
+        $logger->warn(
+            "ECARD: No provisional ecard found with code $verify_code");
+        sleep 2; # Mitigate brute-force attacks
+        return;
+    }
+
+    my $card = $e->search_actor_card({
+        usr => $au->id,
+        barcode => $barcode
+    })->[0];
+
+    if (!$card) {
+        $logger->warn("ECARD: Failed to match verify code ".
+            "($verify_code) with provided barcode ($barcode)");
+        sleep 2; # Mitigate brute-force attacks
+        return;
+    }
+
+    # Verification looks good.  Update the account.
+
+    my $grp = new_editor()->retrieve_permission_grp_tree($FULL_ECARD_GRP);
+
+    $au->profile($grp->id);
+    $au->expire_date(
+        DateTime->now(time_zone => 'local')->add(
+            seconds => interval_to_seconds($grp->perm_interval))->iso8601()
+    );
+
+    $e->xact_begin;
+
+    unless ($e->update_actor_user($au)) {
+        $logger->error("ECARD update failed for $barcode: " . $e->die_event);
+        return;
+    }
+    
+    $e->commit;
+    $logger->info("ECARD: Update to full ecard succeeded for $barcode");
+
+    $ctx->{verify_success} = 1;
+    $ctx->{verify_failed} = 0;
+
+    return;
+}
+
+
+sub log_params {
+    my $self = shift;
+    my $cgi = $self->cgi;
+    my @params = $cgi->param;
+
+    my $msg = '';
+    for my $p (@params) {
+        next if $p =~ /pass/;
+        $msg .= "|" if $msg; 
+        $msg .= "$p=".$cgi->param($p);
+    }
+
+    $logger->info("ECARD: Submit params: $msg");
+}
+
+sub handle_testmode_api {
+    my $self = shift;
+    my $ctx = $self->ctx;
+
+    # Strip data we don't want to publish.
+    my @doc_fields;
+    for my $field_info (@api_fields) {
+        my $doc_info = {};
+        for my $info_key (keys %$field_info) {
+            $doc_info->{$info_key} = $field_info->{$info_key} 
+                unless $info_key eq 'class';
+        }
+        push(@doc_fields, $doc_info);
+    }
+
+    $ctx->{response}->{messages} = [fields => \@doc_fields];
+    $ctx->{response}->{status} = 'API_OK';
+    return $self->compile_response;
+}
+
+sub handle_datamode_api {
+    my $self = shift;
+    my $datamode = shift;
+    my $ctx = $self->ctx;
+
+    if ($datamode =~ /org_units/) {
+        my $orgs = new_editor()->search_actor_org_unit({opac_visible => 't'});
+        my $list = [
+            map { 
+                {name => $_->name, id => $_->id, parent_ou => $_->parent_ou} 
+            } @$orgs
+        ];
+        $ctx->{response}->{messages} = [org_units => $list];
+    }
+
+    $ctx->{response}->{status} = 'DATA_OK';
+    return $self->compile_response;
+}
+
+
+sub load_ecard_submit {
+    my $self = shift;
+    my $ctx = $self->ctx;
+    my $cgi = $self->cgi;
+
+    $self->log_params;
+
+    my $testmode = $cgi->param('testmode') || '';
+    my $datamode = $cgi->param('datamode') || '';
+
+    my $e = $ctx->{editor} = new_editor();
+    $ctx->{response} = {messages => []};
+
+    if ($testmode eq 'CONNECT') {
+        $ctx->{response}->{status} = 'CONNECT_OK';
+        return $self->compile_response;
+    }
+
+    return Apache2::Const::FORBIDDEN unless 
+        $cgi->request_method eq 'POST' &&
+        $self->verify_vendor_host &&
+        $self->login_vendor;
+
+    if ($testmode eq 'AUTH') {
+        # If we got this far, the caller is authorized.
+        $ctx->{response}->{status} = 'AUTH_OK';
+        return $self->compile_response;
+    }
+
+    return $self->handle_testmode_api if $testmode eq 'API';
+    return $self->handle_datamode_api($datamode) if $datamode;
+
+    return $self->compile_response unless $self->make_user;
+    return $self->compile_response unless $self->add_addresses;
+    return $self->compile_response unless $self->add_stat_cats;
+    return $self->compile_response unless $self->check_dupes;
+    return $self->compile_response unless $self->add_card;
+    return $self->compile_response unless $self->save_user;
+    return $self->compile_response if $ctx->{response}->{status};
+
+    $U->create_events_for_hook(
+        'au.create.ecard', $ctx->{user}, $ctx->{user}->home_ou);
+
+    $ctx->{response}->{status} = 'OK';
+    $ctx->{response}->{barcode} = $ctx->{user}->card->barcode;
+
+    return $self->compile_response;
+}
+
+# E-card vendor is not a regular account.  They must have an entry in 
+# the password table with password type ecard_vendor.
+sub login_vendor {
+    my $self = shift;
+    my $username = $self->cgi->param('vendor_username');
+    my $password = $self->cgi->param('vendor_password');
+
+    my $e = new_editor();
+    my $vendor = $e->search_actor_user({usrname => $username})->[0];
+    return 0 unless $vendor;
+
+    return unless $U->verify_user_password(
+        $e, $vendor->id, $password, 'ecard_vendor');
+
+    # Auth checks out OK.  Manually create an authtoken
+
+    my $auth = $U->simplereq(
+        'open-ils.auth_internal',
+        'open-ils.auth_internal.session.create',
+        {user_id => 1, org_unit => 4, login_type => 'temp'}
+    );
+
+    return unless $auth && $auth->{textcode} eq 'SUCCESS';
+
+    $self->ctx->{authtoken} = $auth->{payload}->{authtoken};
+
+    return 1;
+}
+
+sub verify_vendor_host {
+    my $self = shift;
+    # TODO
+    # Confirm calling host matches AOUS ecard.vendor.host
+    # NOTE: we may not have that information inside the firewall.
+    return 1;
+}
+
+sub compile_response {
+    my $self = shift;
+    my $ctx = $self->ctx;
+    $self->apache->content_type("application/json; charset=utf-8");
+    $ctx->{response} = OpenSRF::Utils::JSON->perl2JSON($ctx->{response});
+    $logger->info("ECARD responding with " . $ctx->{response});
+    return Apache2::Const::OK;
+}
+
+my %keep_case = (usrname => 1, passwd => 1, email => 1);
+sub upperclense {
+    my $self = shift;
+    my $field = shift;
+    my $value = shift;
+    $value = uc($value) unless $keep_case{$field};
+    $value = lc($value) if $field eq 'email'; # force it
+    $value =~ s/(^\s*|\s*$)//g;
+    return $value;
+}
+
+
+# Create actor.usr perl object and populate column data
+sub make_user {
+    my $self = shift;
+    my $ctx = $self->ctx;
+    my $cgi = $self->cgi;
+
+    my $au = Fieldmapper::actor::user->new;
+
+    $au->isnew(1);
+    $au->ident_type($ECARD_VERIFY_IDENT); # Ecard Verification
+    $au->net_access_level(101); # No Access
+    $au->ident_value(generate_verify_code());
+
+    $au->profile($PROVISIONAL_ECARD_GRP);
+    my $grp = new_editor()->retrieve_permission_grp_tree($PROVISIONAL_ECARD_GRP);
+
+    $au->expire_date(
+        DateTime->now(time_zone => 'local')->add(
+            seconds => interval_to_seconds($grp->perm_interval))->iso8601()
+    );
+
+    for my $field_info (@api_fields) {
+        my $field = $field_info->{name};
+        next unless $field_info->{class} eq 'au';
+
+        my $val = $cgi->param($field);
+
+        if ($field_info->{required} && !$val) {
+            my $msg = "Value required for field: '$field'";
+            $ctx->{response}->{status} = 'INVALID_PARAMS';
+            push(@{$ctx->{response}->{messages}}, $msg);
+            $logger->error("ECARD $msg");
+        }
+
+        $self->verify_dob($val) if $field eq 'dob' && $val;
+        $au->$field($self->upperclense($field, $val));
+    }
+
+    # Usename defaults to the user barcode
+    return undef if $ctx->{response}->{status}; 
+    return $ctx->{user} = $au;
+}
+
+# Card generation must occur after the user is saved in the DB.
+sub add_card {
+    my $self = shift;
+    my $ctx = $self->ctx;
+    my $user = $ctx->{user};
+
+    my $bc = new_editor()->json_query({from => [
+        'actor.generate_barcode', 
+        '934', # ecard prefix
+        7, # length of autogenated portion
+        'actor.auto_barcode_ecard_seq' # base sequence for autogeneration.
+    ]})->[0];
+
+    my $barcode = $bc->{'actor.generate_barcode'};
+
+    $logger->info("ECARD using generated barcode: $barcode");
+
+    my $card = Fieldmapper::actor::card->new;
+    $card->id(-1);
+    $card->isnew(1);
+    $card->usr($user->id);
+    $card->barcode($barcode);
+
+    # username defaults to barcode
+    $user->usrname($barcode);
+    $user->card($card);
+    $user->cards([$card]);
+
+    return 1;
+}
+
+
+# Returns 1 on success, undef on error.
+sub verify_dob {
+    my $self = shift;
+    my $dob = shift;
+    my $ctx = $self->ctx;
+    my $cgi = $self->cgi;
+
+    my @parts = split(/-/, $dob);
+    my $dob_date;
+
+    eval { # avoid dying on funky dates
+        $dob_date = DateTime->new(
+            year => $parts[0], month => $parts[1], day => $parts[2]);
+    };
+
+    if (!$dob_date || $dob_date > DateTime->now) {
+        my $msg = "Invalid dob: '$dob'";
+        $ctx->{response}->{status} = 'INVALID_PARAMS';
+        push(@{$ctx->{response}->{messages}}, $msg);
+        $logger->error("ECARD $msg");
+        return undef;
+    }
+
+    my $comp_date = DateTime->now;
+    $comp_date->set_hour(0);
+    $comp_date->set_minute(0);
+    $comp_date->set_second(0);
+    $comp_date->subtract(years => 18); # juv age
+
+    if (
+        $dob_date > $comp_date # less than 18 years old
+        && !$cgi->param('ident_value2')) {
+
+        my $msg = "Parent/Guardian (ident_value2) is required for patrons ".
+            "under 18 years of age. dob=$dob";
+        $ctx->{response}->{status} = 'INVALID_PARAMS';
+        push(@{$ctx->{response}->{messages}}, $msg);
+        $logger->error("ECARD $msg");
+        return undef;
+    }
+
+    return 1;
+}
+
+# returns true if the addresses contain all of the same values.
+sub addrs_match {
+    my ($self, $addr1, $addr2) = @_;
+    for my $field ($addr1->real_fields) {
+        return 0 if ($addr1->$field() || '') ne ($addr2->$field() || '');
+    }
+    return 1;
+}
+
+
+sub add_addresses {
+    my $self = shift;
+    my $cgi = $self->cgi;
+    my $ctx = $self->ctx;
+    my $e = $ctx->{editor};
+    my $user = $ctx->{user};
+
+    my $bill_addr = Fieldmapper::actor::user_address->new;
+    $bill_addr->isnew(1);
+    $bill_addr->usr($user->id);
+    $bill_addr->address_type('RESIDENTIAL');
+    $bill_addr->within_city_limits('f');
+
+    # Use as both billing and mailing via virtual ID.
+    $bill_addr->id(-1);
+    $user->billing_address(-1);
+    $user->mailing_address(-1);
+
+    ($cgi->{billing_street1}, $cgi->{billing_street2}) = 
+        OpenILS::Utils::KCLSNormalize::normalize_address_street(
+            $cgi->{billing_street1},
+            $cgi->{billing_street2}
+        );
+
+    # Confirm we have values for all of the required fields.
+    # Apply values to our in-progress address object.
+    for my $field_info (@api_fields) {
+        my $field = $field_info->{name};
+        next unless $field =~ /billing/;
+        next if $field eq 'billing_street1_name';
+
+        my $val = $cgi->param($field);
+
+        if ($field_info->{required} && !$val) {
+            my $msg = "Value required for field: '$field'";
+            $ctx->{response}->{status} = 'INVALID_PARAMS';
+            push(@{$ctx->{response}->{messages}}, $msg);
+            $logger->error("ECARD $msg");
+        }
+
+        (my $col_field = $field) =~ s/billing_//g;
+        $bill_addr->$col_field($self->upperclense($col_field, $val));
+    }
+
+    # exit if there were any errors above.
+    return undef if $ctx->{response}->{status}; 
+
+    $user->billing_address($bill_addr);
+    $user->addresses([$bill_addr]);
+
+    return 1;
+}
+
+sub add_stat_cats {
+    my $self = shift;
+    my $cgi = $self->cgi;
+    my $user = $self->ctx->{user};
+
+    my $ds_map = Fieldmapper::actor::stat_cat_entry_user_map->new;
+    $ds_map->isnew(1);
+    $ds_map->stat_cat(12);
+    $ds_map->stat_cat_entry('KCLS');
+
+    my $events = $cgi->param('events_mailing');
+    my $em_map = Fieldmapper::actor::stat_cat_entry_user_map->new;
+    $em_map->isnew(1);
+    $em_map->stat_cat(3);
+    $em_map->stat_cat_entry($events ? 'Y' : 'N');
+
+    my $foundation = $cgi->param('foundation_mailing');
+    my $fm_map = Fieldmapper::actor::stat_cat_entry_user_map->new;
+    $fm_map->isnew(1);
+    $fm_map->stat_cat(4);
+    $fm_map->stat_cat_entry($foundation ? 'Y' : 'N');
+
+    $user->stat_cat_entries([$ds_map, $em_map, $fm_map]);
+    return 1;
+}
+
+# Returns true if no dupes found, false if dupes are found.
+sub check_dupes {
+    my $self = shift;
+    my $ctx  = $self->ctx;
+    my $user = $ctx->{user};
+    my $addr = $user->addresses->[0];
+    my $e = new_editor();
+
+    my @dupe_patron_fields = 
+        qw/first_given_name family_name dob/;
+
+    my $search = {
+        first_given_name => {value => $user->first_given_name, group => 0},
+        family_name => {value => $user->family_name, group => 0},
+        dob => {value => substr($user->dob, 0, 4), group => 0} # birth year
+    };
+
+    my $root_org = $e->search_actor_org_unit({parent_ou => undef})->[0];
+
+    my $ids = $U->storagereq(
+        "open-ils.storage.actor.user.crazy_search", 
+        $search,
+        1000,           # search limit
+        undef,          # sort
+        1,              # include inactive
+        $root_org->id,  # ws_ou
+        $root_org->id   # search_ou
+    );
+
+    return 1 if @$ids == 0;
+
+    $logger->info("ECARD found potential duplicate patrons: @$ids");
+
+    if (my $streetname = $self->cgi->param('billing_street1_name')) {
+        # We found matching patrons.  Perform a secondary check on the
+        # address street name only.
+
+        $logger->info("ECARD secondary search on street name: $streetname");
+
+        my $addr_ids = $e->search_actor_user_address(
+            {   usr => $ids,
+                street1 => {'~*' => "(^| )$streetname( |\$)"}
+            }, {idlist => 1}
+        );
+
+        if (@$addr_ids) {
+            # we don't really care what patrons match at this point,
+            # only whether a match is found.
+            $ids = [1];
+            $logger->info("ECARD secondary address check match(es) ".
+                "found on address(es) @$addr_ids");
+
+        } else {
+            $ids = [];
+            $logger->info(
+                "ECARD secondary address check found no matches");
+        }
+
+    } else {
+        $ids = [];
+        # unclear if this is a possibility -- err on the side of allowing
+        # the registration.
+        $logger->info("ECARD found possible patron match but skipping ".
+            "secondary street name check -- no street name was provided");
+    }
+
+    return 1 if @$ids == 0;
+
+    $ctx->{response}->{status} = 'DUPLICATE';
+    $ctx->{response}->{messages} = ['first_given_name', 
+        'familiy_name', 'dob_year', 'billing_street1_name'];
+    return undef;
+}
+
+
+sub save_user {
+    my $self = shift;
+    my $ctx = $self->ctx;
+    my $cgi = $self->cgi;
+    my $user = $ctx->{user};
+
+    my $resp = $U->simplereq(
+        'open-ils.actor',
+        'open-ils.actor.patron.update',
+        $self->ctx->{authtoken}, $user
+    );
+
+    $resp = {textcode => 'UNKNOWN_ERROR'} unless $resp;
+
+    if ($U->is_event($resp)) {
+
+        my $msg = "Error creating user account: " . $resp->{textcode};
+        $logger->error("ECARD: $msg");
+
+        $ctx->{response}->{status} = 'CREATE_ERR';
+        $ctx->{response}->{messages} = [{msg => $msg, pid => $$}];
+
+        return 0;
+    }
+
+    $ctx->{user} = $resp;
+    return 1;
+}
+
+my %bc_parts; # cache
+my @bc_part_keys = qw/css screen_reader_navigation header footer js/;
+sub collect_header_footer {
+    my $self = shift;
+
+    # kiosk == no header/footer
+    return if $self->cgi->param('kiosk');
+
+    if ($bc_parts{header}) {
+        $self->ctx->{"bc_$_"} = $bc_parts{$_} for @bc_part_keys;
+        return;
+    }
+
+    my $agent = LWP::UserAgent->new(timeout => 5);
+    my $res = $agent->get($HEADER_FOOTER_URL); 
+    $logger->info("Self-reg header/footer request returned code ".$res->code);
+    
+    if (!$res->is_success) {
+        $logger->error("Self-reg header/footer request ".
+          "[$HEADER_FOOTER_URL] failed with error " . $res->status_line);
+
+        return;
+    }
+
+    my $json = $res->content;
+
+    if (!$json) {
+        $logger->error("Self-reg header/footer ".
+            "[$HEADER_FOOTER_URL] returned an empty response");
+        return;
+    }
+        
+
+    my $blob;
+    eval { $blob = OpenSRF::Utils::JSON->JSON2perl($json) };
+
+    if ($@) {
+        $logger->error("Self-reg header/footer ".
+            "[$HEADER_FOOTER_URL] returned invalid JSON : $@");
+        return;
+    }
+
+    $self->ctx->{"bc_$_"} = $bc_parts{$_} = $blob->{$_} for @bc_part_keys;
+}
+
+1;
+
index f12c2ec..6e5d7c4 100644 (file)
@@ -1418,6 +1418,8 @@ function trimGrpTree(autoWidget) {
         901,  //Student Ecard
         903,  //Teacher Ecard
         902,  //Classroom Databases
+        951,  //Temporary Ecard
+        952,  //Ecard
         40,   //ILL
         89,   //CMS Recall 89
         91,   //Enumclaw Migration