From 7bd7fa06b034fa3a5891f203346b93c156a8e3d8 Mon Sep 17 00:00:00 2001
From: Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
Date: Fri, 11 Mar 2016 15:00:01 +0100
Subject: [PATCH] Bug 6906 - show 'Borrower has previously issued...
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

New feature: provide granular means to configure warnings about items
that have been issued to a particular borrower before, according to
their checkout history.

- Global syspref ('CheckPrevCheckout'), set to 'hardno' by default,
  allows users to enable this feature library wide.
- Per patron category pref allows libraries to create overrides per
  category, falling back on the global setting by default.
- Per patron pref allows switching the functionality on at the level
  of patron. Fall-back to category settings by default.

* Koha/Patron (wantsCheckPrevCheckout, doCheckPrevCheckout): New
  methods.
* C4/Circulation.pm (CanBookBeIssued): Introduce CheckPrevCheckout
  check.
* admin/categories.pl: Pass along checkprevcheckout.
* koha-tmpl/intranet-tmpl/prog/en/modules/admin/categories.tt: Expose
  CheckPrevCheckout per category setting.
* koha-tmpl/intranet-tmpl/prog/en/modules/preferences/patrons.pref:
  Expose CheckPrevCheckout syspref.
* koha-tmpl/intranet-tmpl/prog/en/modules/members/memberentrygen.tt:
  Expose per patron CheckPrevCheckout preference.
* koha-tmpl/intranet-tmpl/prog/en/modules/members/moremember.tt: Expose
  per patron CheckPrevCheckout preference.
* koha-tmpl/intranet-tmpl/prog/en/modules/circ/circulation.tt: Add
  'CHECKPREVCHECKOUT' confirmation message.
* installer/data/mysql/kohastructure.sql: Modify structure of
  'categories', 'borrowers', 'oldborrowers'.
* installer/data/mysql/sysprefs.sql: Add 'CheckPrevCheckout'.
* installer/data/mysql/atomicupdate/checkPrevCheckout.sql: New file.
* t/db_dependent/Patron/CheckPrevCheckout.t: New file with unit tests.

Test plan:
- Apply patch.
- Run updatedatabase.
- Regenerate Koha Schema files.
- Run the unit tests.
- Verify 'CheckPrevCheckout' is visible in Patrons sysprefs and can be
  switched to 'hardyes', 'softyes', 'softno' and 'hardno'.
  + Check out previously checked out items to a patron, checking the
    message appears as expected.
- Verify no 'Check previous checkouts' setting appears on the borrower
  category pages if the syspref is set to a 'hard' option.
- Verify 'Check previous checkouts' setting appears on the borrower
  category pages and can be modified per borrower category.
  + Issue previously issued items to a borrower, checking the message
    appears as expected (This setting should override the default
    setting if that is set to a 'soft' option).
- Verify no 'Check previous checkouts' setting appears on the individual
  borrower pages if the syspref is set to a 'hard' option.
- Verify 'Check previous checkouts' setting appears on individual
  borrower pages and can be modified.
  + Issue previously issued items to a borrower, checking the message
    appears as expected (This setting should override the category
    setting and the default setting if the latter is set to a 'soft'
    option).

Followed test plan, works as expected.
Signed-off-by: Marc Véron <veron@veron.ch>
---
 C4/Circulation.pm                                  |    8 +
 Koha/Patron.pm                                     |   76 +++-
 admin/categories.pl                                |    3 +
 .../data/mysql/atomicupdate/checkPrevCheckout.sql  |   14 +
 installer/data/mysql/kohastructure.sql             |    3 +
 installer/data/mysql/sysprefs.sql                  |    1 +
 .../prog/en/modules/admin/categories.tt            |   53 +++
 .../prog/en/modules/admin/preferences/patrons.pref |    9 +
 .../prog/en/modules/circ/circulation.tt            |    4 +
 .../prog/en/modules/members/memberentrygen.tt      |   21 +-
 .../prog/en/modules/members/moremember.tt          |   11 +
 t/db_dependent/Patron/CheckPrevCheckout.t          |  447 ++++++++++++++++++++
 12 files changed, 648 insertions(+), 2 deletions(-)
 create mode 100644 installer/data/mysql/atomicupdate/checkPrevCheckout.sql
 create mode 100644 t/db_dependent/Patron/CheckPrevCheckout.t

diff --git a/C4/Circulation.pm b/C4/Circulation.pm
index 8d585a2..6e4c473 100644
--- a/C4/Circulation.pm
+++ b/C4/Circulation.pm
@@ -941,6 +941,14 @@ sub CanBookBeIssued {
     }
 
     #
+    # CHECKPREVCHECKOUT: CHECK IF ITEM HAS EVER BEEN LENT TO PATRON
+    #
+    my $patron = Koha::Patrons->find($borrower->{borrowernumber});
+    my $wantsCheckPrevCheckout = $patron->wantsCheckPrevCheckout;
+    $needsconfirmation{PREVISSUE} = 1
+        if ($wantsCheckPrevCheckout and $patron->doCheckPrevCheckout($item));
+
+    #
     # ITEM CHECKING
     #
     if ( $item->{'notforloan'} )
diff --git a/Koha/Patron.pm b/Koha/Patron.pm
index ac62594..aa38363 100644
--- a/Koha/Patron.pm
+++ b/Koha/Patron.pm
@@ -1,6 +1,7 @@
 package Koha::Patron;
 
 # Copyright ByWater Solutions 2014
+# Copyright PTFS Europe 2016
 #
 # This file is part of Koha.
 #
@@ -21,9 +22,13 @@ use Modern::Perl;
 
 use Carp;
 
+use C4::Context;
 use Koha::Database;
-use Koha::Patrons;
+use Koha::Issues;
+use Koha::OldIssues;
+use Koha::Patron::Categories;
 use Koha::Patron::Images;
+use Koha::Patrons;
 
 use base qw(Koha::Object);
 
@@ -95,6 +100,74 @@ sub siblings {
     );
 }
 
+=head3 wantsCheckPrevCheckout
+
+    $wantsCheckPrevCheckout = $patron->wantsCheckPrevCheckout;
+
+Return 1 if Koha needs to perform PrevIssue checking, else 0.
+
+=cut
+
+sub wantsCheckPrevCheckout {
+    my ( $self ) = @_;
+    my $syspref = C4::Context->preference("checkPrevCheckout");
+
+    # Simple cases
+    ## Hard syspref trumps all
+    return 1 if ($syspref eq 'hardyes');
+    return 0 if ($syspref eq 'hardno');
+    ## Now, patron pref trumps all
+    return 1 if ($self->checkprevcheckout eq 'yes');
+    return 0 if ($self->checkprevcheckout eq 'no');
+
+    # More complex: patron inherits -> determine category preference
+    my $checkPrevCheckoutByCat = Koha::Patron::Categories
+        ->find($self->categorycode)->checkprevcheckout;
+    return 1 if ($checkPrevCheckoutByCat eq 'yes');
+    return 0 if ($checkPrevCheckoutByCat eq 'no');
+
+    # Finally: category preference is inherit, default to 0
+    if ($syspref eq 'softyes') {
+        return 1;
+    } else {
+        return 0;
+    }
+}
+
+=head3 doCheckPrevCheckout
+
+    $checkPrevCheckout = $patron->doCheckPrevCheckout($item);
+
+Return 1 if the bib associated with $ITEM has previously been checked out to
+$PATRON, 0 otherwise.
+
+=cut
+
+sub doCheckPrevCheckout {
+    my ( $self, $item ) = @_;
+
+    # Find all items for bib and extract item numbers.
+    my @items = Koha::Items->search({biblionumber => $item->{biblionumber}});
+    my @item_nos;
+    foreach my $item (@items) {
+        push @item_nos, $item->itemnumber;
+    }
+
+    # Create (old)issues search criteria
+    my $criteria = {
+        borrowernumber => $self->borrowernumber,
+        itemnumber => \@item_nos,
+    };
+
+    # Check current issues table
+    my $issues = Koha::Issues->search($criteria);
+    return 1 if $issues->count; # 0 || N
+
+    # Check old issues table
+    my $old_issues = Koha::OldIssues->search($criteria);
+    return $old_issues->count;  # 0 || N
+}
+
 =head3 type
 
 =cut
@@ -106,6 +179,7 @@ sub _type {
 =head1 AUTHOR
 
 Kyle M Hall <kyle@bywatersolutions.com>
+Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
 
 =cut
 
diff --git a/admin/categories.pl b/admin/categories.pl
index 7d4e8d3..dfcbcb4 100755
--- a/admin/categories.pl
+++ b/admin/categories.pl
@@ -90,6 +90,7 @@ elsif ( $op eq 'add_validate' ) {
     my $overduenoticerequired = $input->param('overduenoticerequired');
     my $category_type = $input->param('category_type');
     my $BlockExpiredPatronOpacActions = $input->param('BlockExpiredPatronOpacActions');
+    my $checkPrevCheckout = $input->param('checkprevcheckout');
     my $default_privacy = $input->param('default_privacy');
     my @branches = grep { $_ ne q{} } $input->multi_param('branches');
 
@@ -113,6 +114,7 @@ elsif ( $op eq 'add_validate' ) {
         $category->overduenoticerequired($overduenoticerequired);
         $category->category_type($category_type);
         $category->BlockExpiredPatronOpacActions($BlockExpiredPatronOpacActions);
+        $category->checkprevcheckout($checkPrevCheckout);
         $category->default_privacy($default_privacy);
         eval {
             $category->store;
@@ -138,6 +140,7 @@ elsif ( $op eq 'add_validate' ) {
             overduenoticerequired => $overduenoticerequired,
             category_type => $category_type,
             BlockExpiredPatronOpacActions => $BlockExpiredPatronOpacActions,
+            checkprevcheckout => $checkPrevCheckout,
             default_privacy => $default_privacy,
         });
         eval {
diff --git a/installer/data/mysql/atomicupdate/checkPrevCheckout.sql b/installer/data/mysql/atomicupdate/checkPrevCheckout.sql
new file mode 100644
index 0000000..2e6fd7e
--- /dev/null
+++ b/installer/data/mysql/atomicupdate/checkPrevCheckout.sql
@@ -0,0 +1,14 @@
+INSERT INTO systempreferences (variable,value,options,explanation,type)
+VALUES('CheckPrevCheckout','hardno','hardyes|softyes|softno|hardno','By default, for every item checked out, should we warn if the patron has checked out that item in the past?','Choice');
+
+ALTER TABLE categories
+ADD COLUMN `checkprevcheckout` varchar(7) NOT NULL default 'inherit'
+AFTER `default_privacy`;
+
+ALTER TABLE borrowers
+ADD COLUMN `checkprevcheckout` varchar(7) NOT NULL default 'inherit'
+AFTER `privacy_guarantor_checkouts`;
+
+ALTER TABLE deletedborrowers
+ADD COLUMN `checkprevcheckout` varchar(7) NOT NULL default 'inherit'
+AFTER `privacy_guarantor_checkouts`;
diff --git a/installer/data/mysql/kohastructure.sql b/installer/data/mysql/kohastructure.sql
index 1d3b3d6..788c2d6 100644
--- a/installer/data/mysql/kohastructure.sql
+++ b/installer/data/mysql/kohastructure.sql
@@ -266,6 +266,7 @@ CREATE TABLE `borrowers` ( -- this table includes information about your patrons
   `sms_provider_id` int(11) DEFAULT NULL, -- the provider of the mobile phone number defined in smsalertnumber
   `privacy` integer(11) DEFAULT '1' NOT NULL, -- patron/borrower's privacy settings related to their reading history
   `privacy_guarantor_checkouts` tinyint(1) NOT NULL DEFAULT '0', -- controls if relatives can see this patron's checkouts
+  `checkprevcheckout` varchar(7) NOT NULL default 'inherit', -- produce a warning for this patron if this item has previously been checked out to this patron if 'yes', not if 'no', defer to category setting if 'inherit'.
   UNIQUE KEY `cardnumber` (`cardnumber`),
   PRIMARY KEY `borrowernumber` (`borrowernumber`),
   KEY `categorycode` (`categorycode`),
@@ -504,6 +505,7 @@ CREATE TABLE `categories` ( -- this table shows information related to Koha patr
   `category_type` varchar(1) NOT NULL default 'A', -- type of Koha patron (Adult, Child, Professional, Organizational, Statistical, Staff)
   `BlockExpiredPatronOpacActions` tinyint(1) NOT NULL default '-1', -- wheither or not a patron of this category can renew books or place holds once their card has expired. 0 means they can, 1 means they cannot, -1 means use syspref BlockExpiredPatronOpacActions
   `default_privacy` ENUM( 'default', 'never', 'forever' ) NOT NULL DEFAULT 'default', -- Default privacy setting for this patron category
+  `checkprevcheckout` varchar(7) NOT NULL default 'inherit', -- produce a warning for this patron category if this item has previously been checked out to this patron if 'yes', not if 'no', defer to syspref setting if 'inherit'.
   PRIMARY KEY  (`categorycode`),
   UNIQUE KEY `categorycode` (`categorycode`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
@@ -920,6 +922,7 @@ CREATE TABLE `deletedborrowers` ( -- stores data related to the patrons/borrower
   `sms_provider_id` int(11) DEFAULT NULL, -- the provider of the mobile phone number defined in smsalertnumber
   `privacy` integer(11) DEFAULT '1' NOT NULL, -- patron/borrower's privacy settings related to their reading history  KEY `borrowernumber` (`borrowernumber`),
   `privacy_guarantor_checkouts` tinyint(1) NOT NULL DEFAULT '0', -- controls if relatives can see this patron's checkouts
+  `checkprevcheckout` varchar(7) NOT NULL default 'inherit', -- produce a warning for this patron if this item has previously been checked out to this patron if 'yes', not if 'no', defer to category setting if 'inherit'.
   KEY borrowernumber (borrowernumber),
   KEY `cardnumber` (`cardnumber`),
   KEY `sms_provider_id` (`sms_provider_id`)
diff --git a/installer/data/mysql/sysprefs.sql b/installer/data/mysql/sysprefs.sql
index d95754d..c21c6d3 100644
--- a/installer/data/mysql/sysprefs.sql
+++ b/installer/data/mysql/sysprefs.sql
@@ -91,6 +91,7 @@ INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, `
 ('CatalogModuleRelink','0',NULL,'If OFF the linker will never replace the authids that are set in the cataloging module.','YesNo'),
 ('CataloguingLog','1',NULL,'If ON, log edit/create/delete actions on bibliographic data. WARNING: this feature is very resource consuming.','YesNo'),
 ('checkdigit','none','none|katipo','If ON, enable checks on patron cardnumber: none or \"Katipo\" style checks','Choice'),
+('CheckPrevCheckout','hardno','hardyes|softyes|softno|hardno','By default, for every item checked out, should we warn if the patron has borrowed that item in the past?','Choice'),
 ('CircAutocompl','1',NULL,'If ON, autocompletion is enabled for the Circulation input','YesNo'),
 ('CircAutoPrintQuickSlip','qslip',NULL,'Choose what should happen when an empty barcode field is submitted in circulation: Display a print quick slip window, Display a print slip window or Clear the screen.','Choice'),
 ('CircControl','ItemHomeLibrary','PickupLibrary|PatronLibrary|ItemHomeLibrary','Specify the agency that controls the circulation and fines policy','Choice'),
diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/categories.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/categories.tt
index aa2d3a2..af6cecd 100644
--- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/categories.tt
+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/categories.tt
@@ -189,6 +189,28 @@
                         Choose whether patrons of this category be blocked from public catalog actions such as renewing and placing holds when their cards have expired.
                     </span>
                 </li>
+                [% IF ( Koha.Preference('CheckPrevCheckout') == 'softyes' || Koha.Preference('CheckPrevCheckout') == 'softno' )  %]
+                  <li><label for="checkprevcheckout">Check for previous checkouts: </label>
+                      <select name="checkprevcheckout" id="checkprevcheckout">
+                          [% IF category.checkprevcheckout == 'yes' %]
+                          <option value="yes" selected="selected">Yes and try to override system preferences</option>
+                          <option value="no">No and try to override system preferences</option>
+                          <option value="inherit">Inherit from system preferences</option>
+                          [% ELSIF category.checkprevcheckout == 'no' %]
+                          <option value="yes">Yes and try to override system preferences</option>
+                          <option value="no" selected="selected">No and try to override system preferences</option>
+                          <option value="inherit">Inherit from system preferences</option>
+                          [% ELSE %]
+                          <option value="yes">Yes and try to override system preferences</option>
+                          <option value="no">No and try to override system preferences</option>
+                          <option value="inherit" selected="selected">Inherit from system preferences</option>
+                          [% END %]
+                      </select>
+                      <span>
+                          Choose whether patrons of this category by default are reminded if they try to borrow an item they borrowed before.
+                      </span>
+                  </li>
+                [% END %]
                 <li>
                     <label for="default_privacy">Default privacy: </label>
                     <select id="default_privacy" name="default_privacy">
@@ -261,6 +283,22 @@
                 <tr><th scope="row">Receives overdue notices: </th><td>[% IF category. overduenoticerequired %]Yes[% ELSE %]No[% END %]</td></tr>
                 <tr><th scope="row">Lost items in staff client</th><td>[% IF category.hidelostitems %]Hidden by default[% ELSE %]Shown[% END %]</td></tr>
                 <tr><th scope="row">Hold fee: </th><td>[% category.reservefee | $Price %]</td></tr>
+
+                [% IF ( Koha.Preference('CheckPrevCheckout') == 'softyes' || Koha.Preference('CheckPrevCheckout') == 'softno' ) %]
+                  <tr>
+                      <th scope="row">Check previous checkouts: </th>
+                      <td>
+                          [% SWITCH category.checkprevcheckout %]
+                          [% CASE 'yes' %]
+                              Yes
+                          [% CASE 'no' %]
+                              No
+                          [% CASE 'inherit' %]
+                              Inherit
+                          [% END %]
+                      </td>
+                  </tr>
+                [% END %]
                 <tr>
                     <th scope="row">Default privacy: </th>
                     <td>
@@ -317,6 +355,9 @@
                     <th scope="col">Messaging</th>
                     [% END %]
                     <th scope="col">Branches limitations</th>
+                    [% IF ( Koha.Preference('CheckPrevCheckout') == 'softyes' || Koha.Preference('CheckPrevCheckout') == 'softno' ) %]
+                    <th scope="col">Check previous checkout?</th>
+                    [% END %]
                     <th scope="col">Default privacy</th>
                     <th scope="col">&nbsp; </th>
                     <th scope="col">&nbsp; </th>
@@ -394,6 +435,18 @@
                                 No limitation
                             [% END %]
                         </td>
+                        [% IF ( Koha.Preference('CheckPrevCheckout') == 'softyes' || Koha.Preference('CheckPrevCheckout') == 'softno' ) %]
+                          <td>
+                              [% SWITCH category.checkprevcheckout %]
+                              [% CASE 'yes' %]
+                              Yes
+                              [% CASE 'no' %]
+                              No
+                              [% CASE 'inherit' %]
+                              Inherit
+                              [% END %]
+                          </td>
+                        [% END %]
                         <td>
                             [% SWITCH category.default_privacy %]
                             [% CASE 'default' %]
diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/patrons.pref b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/patrons.pref
index f7b5706..40900ec 100644
--- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/patrons.pref
+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/patrons.pref
@@ -44,6 +44,15 @@ Patrons:
            class: multi
          - (separate multiple choices with |)
      -
+         - pref: CheckPrevCheckout
+           default: no
+           choices:
+               hardyes: "Do"
+               softyes: "Unless overridden, do"
+               softno: "Unless overridden, do not"
+               hardno: "Do not"
+         - " check borrower checkout history to see if the current item has been checked out before."
+     -
          - pref: checkdigit
            choices:
                none: "Don't"
diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/circ/circulation.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/circ/circulation.tt
index cd3fc35..638504c 100644
--- a/koha-tmpl/intranet-tmpl/prog/en/modules/circ/circulation.tt
+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/circ/circulation.tt
@@ -321,6 +321,10 @@ $(document).ready(function() {
     <li>High demand item. Loan period shortened to [% HIGHHOLDS.duration %] days (due [% HIGHHOLDS.returndate %]). Check out anyway?</li>
 [% END %]
 
+[% IF PREVISSUE %]
+    <li>This item has previously been checked out to this patron.  Check out anyway?</li>
+[% END %]
+
 [% IF BIBLIO_ALREADY_ISSUED %]
   <li>
     Patron has already checked out another item from this record.
diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/members/memberentrygen.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/members/memberentrygen.tt
index aa1de3e..a5fa3f7 100644
--- a/koha-tmpl/intranet-tmpl/prog/en/modules/members/memberentrygen.tt
+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/members/memberentrygen.tt
@@ -604,7 +604,26 @@ $(document).ready(function() {
             [% END %]
         </li>
     [% END %]
-	</ol>
+    [% IF ( Koha.Preference('CheckPrevCheckout') == 'softyes' || Koha.Preference('CheckPrevCheckout') == 'softno' ) %]
+      <li><label for="checkprevcheckout">Check for previous checkouts: </label>
+        <select name="checkprevcheckout" id="checkprevcheckout">
+        [% IF ( checkprevcheckout == 'yes' ) %]
+          <option value="yes" selected="selected">Yes if settings allow it</option>
+          <option value="no">No if settings allow it</option>
+          <option value="inherit">Inherit from settings</option>
+        [% ELSIF ( checkprevcheckout == 'no' ) %]
+          <option value="yes">Yes if settings allow it</option>
+          <option value="no" selected="selected">No if settings allow it</option>
+          <option value="inherit">Inherit from settings</option>
+        [% ELSE %]
+          <option value="yes">Yes if settings allow it</option>
+          <option value="no">No if settings allow it</option>
+          <option value="inherit" selected="selected">Inherit from settings</option>
+        [% END %]
+        </select>
+       </li>
+     [% END %]
+   </ol>
   </fieldset>
     [% UNLESS nodateenrolled &&  noopacnote && noborrowernotes %]
 	<fieldset class="rows" id="memberentry_subscription">
diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/members/moremember.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/members/moremember.tt
index b775673..92231ca 100644
--- a/koha-tmpl/intranet-tmpl/prog/en/modules/members/moremember.tt
+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/members/moremember.tt
@@ -404,6 +404,17 @@ function validate1(date) {
             <li><span class="label">Activate sync: </span>No</li>
         [% END %]
     [% END %]
+    [% IF ( Koha.Preference('CheckPrevCheckout') == 'softyes' || Koha.Preference('CheckPrevCheckout') == 'softno' ) %]
+      <li><span class="label">Check previous checkouts: </span>
+        [% IF ( checkprevcheckout == 'yes' ) %]
+        Yes
+        [% ELSIF ( checkprevcheckout == 'no' ) %]
+        No
+        [% ELSE %]
+        Inherited
+        [% END %]
+      </li>
+    [% END %]
 	</ol>
 	</div>
  </div>
diff --git a/t/db_dependent/Patron/CheckPrevCheckout.t b/t/db_dependent/Patron/CheckPrevCheckout.t
new file mode 100644
index 0000000..d76b96d0
--- /dev/null
+++ b/t/db_dependent/Patron/CheckPrevCheckout.t
@@ -0,0 +1,447 @@
+#!/usr/bin/perl
+use Modern::Perl;
+
+use C4::Members;
+use C4::Circulation;
+use Koha::Database;
+use Koha::Patrons;
+use Koha::Patron;
+
+use Test::More tests => 59;
+
+use_ok('Koha::Patron');
+
+use t::lib::TestBuilder;
+use t::lib::Mocks;
+
+my $schema = Koha::Database->new->schema;
+$schema->storage->txn_begin;
+
+my $builder = t::lib::TestBuilder->new;
+my $yesCatCode = $builder->build({
+    source => 'Category',
+    value => {
+        categorycode => 'yesCat',
+        checkprevcheckout => 'yes',
+    },
+});
+
+my $noCatCode = $builder->build({
+    source => 'Category',
+    value => {
+        categorycode => 'noCat',
+        checkprevcheckout => 'no',
+    },
+});
+
+my $inheritCatCode = $builder->build({
+    source => 'Category',
+    value => {
+        categorycode => 'inheritCat',
+        checkprevcheckout => 'inherit',
+    },
+});
+
+# Create context for some tests late on in the file.
+my $staff = $builder->build({source => 'Borrower'});
+my @USERENV = (
+    $staff->{borrowernumber}, 'test', 'MASTERTEST', 'firstname', 'CPL',
+    'CPL', 'email@example.org'
+);
+C4::Context->_new_userenv('DUMMY_SESSION_ID');
+C4::Context->set_userenv(@USERENV);
+BAIL_OUT("No userenv") unless C4::Context->userenv;
+
+
+# wantsCheckPrevCheckout
+
+# We expect the following result matrix:
+#
+# (1/0 indicates the return value of WantsCheckPrevCheckout; i.e. 1 says we
+# should check whether the item was previously issued)
+#
+# | System Preference | hardyes                           | softyes                           | softno                            | hardno                            |
+# |-------------------+-----------------------------------+-----------------------------------+-----------------------------------+-----------------------------------|
+# | Category Setting  | yes       | no        | inherit   | yes       | no        | inherit   | yes       | no        | inherit   | yes       | no        | inherit   |
+# |-------------------+-----------+-----------+-----------+-----------+-----------+-----------+-----------+-----------+-----------+-----------+-----------+-----------|
+# | Patron Setting    | y | n | i | y | n | i | y | n | i | y | n | i | y | n | i | y | n | i | y | n | i | y | n | i | y | n | i | y | n | i | y | n | i | y | n | i |
+# |-------------------+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
+# | Expected Result   | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 1 | 1 | 0 | 0 | 1 | 0 | 1 | 1 | 0 | 1 | 1 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
+
+my $mappings = [
+    {
+        syspref    => 'hardyes',
+        categories => [
+            {
+                setting => 'yes',
+                patrons => [
+                    {setting => 'yes',     result => 1},
+                    {setting => 'no',      result => 1},
+                    {setting => 'inherit', result => 1},
+                ],
+            },
+            {
+                setting => 'no',
+                patrons => [
+                    {setting => 'yes',     result => 1},
+                    {setting => 'no',      result => 1},
+                    {setting => 'inherit', result => 1},
+                ],
+            },
+            {
+                setting => 'inherit',
+                patrons => [
+                    {setting => 'yes',     result => 1},
+                    {setting => 'no',      result => 1},
+                    {setting => 'inherit', result => 1},
+                ],
+            },
+        ],
+    },
+    {
+        syspref    => 'softyes',
+        categories => [
+            {
+                setting => 'yes',
+                patrons => [
+                    {setting => 'yes',     result => 1},
+                    {setting => 'no',      result => 0},
+                    {setting => 'inherit', result => 1},
+                ],
+            },
+            {
+                setting => 'no',
+                patrons => [
+                    {setting => 'yes',     result => 1},
+                    {setting => 'no',      result => 0},
+                    {setting => 'inherit', result => 0},
+                ],
+            },
+            {
+                setting => 'inherit',
+                patrons => [
+                    {setting => 'yes',     result => 1},
+                    {setting => 'no',      result => 0},
+                    {setting => 'inherit', result => 1},
+                ],
+            },
+        ],
+    },
+    {
+        syspref    => 'softno',
+        categories => [
+            {
+                setting => 'yes',
+                patrons => [
+                    {setting => 'yes',     result => 1},
+                    {setting => 'no',      result => 0},
+                    {setting => 'inherit', result => 1},
+                ],
+            },
+            {
+                setting => 'no',
+                patrons => [
+                    {setting => 'yes',     result => 1},
+                    {setting => 'no',      result => 0},
+                    {setting => 'inherit', result => 0},
+                ],
+            },
+            {
+                setting => 'inherit',
+                patrons => [
+                    {setting => 'yes',     result => 1},
+                    {setting => 'no',      result => 0},
+                    {setting => 'inherit', result => 0},
+                ],
+            },
+        ],
+    },
+    {
+        syspref    => 'hardno',
+        categories => [
+            {
+                setting => 'yes',
+                patrons => [
+                    {setting => 'yes',     result => 0},
+                    {setting => 'no',      result => 0},
+                    {setting => 'inherit', result => 0},
+                ],
+            },
+            {
+                setting => 'no',
+                patrons => [
+                    {setting => 'yes',     result => 0},
+                    {setting => 'no',      result => 0},
+                    {setting => 'inherit', result => 0},
+                ],
+            },
+            {
+                setting => 'inherit',
+                patrons => [
+                    {setting => 'yes',     result => 0},
+                    {setting => 'no',      result => 0},
+                    {setting => 'inherit', result => 0},
+                ],
+            },
+        ],
+    },
+];
+
+map {
+    my $syspref = $_->{syspref};
+    t::lib::Mocks::mock_preference('checkprevcheckout', $syspref);
+    map {
+        my $code = $_->{setting} . 'Cat';
+        map {
+            my $kpatron = $builder->build({
+                source => 'Borrower',
+                value  => {
+                    checkprevcheckout => $_->{setting},
+                    categorycode => $code,
+                },
+            });
+            my $patron = Koha::Patrons->find($kpatron->{borrowernumber});
+            is(
+                $patron->wantsCheckPrevCheckout, $_->{result},
+                "Predicate with syspref " . $syspref . ", cat " . $code
+                    . ", patron " . $_->{setting}
+              );
+        } @{$_->{patrons}};
+    } @{$_->{categories}};
+} @{$mappings};
+
+# doCheckPrevCheckout
+
+# We want to test:
+# - DESCRIPTION [RETURNVALUE (0/1)]
+## PreIssue (sanity checks)
+# - Item, patron [0]
+# - Diff item, same bib, same patron [0]
+# - Diff item, diff bib, same patron [0]
+# - Same item, diff patron [0]
+# - Diff item, same bib, diff patron [0]
+# - Diff item, diff bib, diff patron [0]
+## PostIssue
+# - Same item, same patron [1]
+# - Diff item, same bib, same patron [1]
+# - Diff item, diff bib, same patron [0]
+# - Same item, diff patron [0]
+# - Diff item, same bib, diff patron [0]
+# - Diff item, diff bib, diff patron [0]
+## PostReturn
+# - Same item, same patron [1]
+# - Diff item, same bib, same patron [1]
+# - Diff item, diff bib, same patron [0]
+# - Same item, diff patron [0]
+# - Diff item, same bib, diff patron [0]
+# - Diff item, diff bib, diff patron [0]
+
+# Requirements:
+# $patron, $different_patron, $items (same bib number), $different_item
+my $patron = $builder->build({source => 'Borrower'});
+my $patron_d = $builder->build({source => 'Borrower'});
+my $item_1 = $builder->build({source => 'Item'});
+my $item_2 = $builder->build({
+    source => 'Item',
+    value => { biblionumber => $item_1->{biblionumber} },
+});
+my $item_d = $builder->build({source => 'Item'});
+
+## Testing Sub
+sub test_it {
+    my ($mapping, $stage) = @_;
+    map {
+        my $patron = Koha::Patrons->find($_->{patron}->{borrowernumber});
+        is(
+            $patron->doCheckPrevCheckout($_->{item}),
+            $_->{result}, $stage . ": " . $_->{msg}
+        );
+    } @{$mapping};
+};
+
+## Initial Mappings
+my $cpvmappings = [
+    {
+        msg => "Item, patron [0]",
+        item => $item_1,
+        patron => $patron,
+        result => 0,
+    },
+    {
+        msg => "Diff item, same bib, same patron [0]",
+        item => $item_2,
+        patron => $patron,
+        result => 0,
+    },
+    {
+        msg => "Diff item, diff bib, same patron [0]",
+        item => $item_d,
+        patron => $patron,
+        result => 0,
+    },
+    {
+        msg => "Same item, diff patron [0]",
+        item => $item_1,
+        patron => $patron_d,
+        result => 0,
+    },
+    {
+        msg => "Diff item, same bib, diff patron [0]",
+        item => $item_2,
+        patron => $patron_d,
+        result => 0,
+    },
+    {
+        msg => "Diff item, diff bib, diff patron [0]",
+        item => $item_d,
+        patron => $patron_d,
+        result => 0,
+    },
+];
+
+test_it($cpvmappings, "PreIssue");
+
+# Issue item_1 to $patron:
+my $patron_get_mem =
+    GetMember(%{{borrowernumber => $patron->{borrowernumber}}});
+BAIL_OUT("Issue failed")
+    unless AddIssue($patron_get_mem, $item_1->{barcode});
+
+# Then test:
+my $cpvPmappings = [
+    {
+        msg => "Same item, same patron [1]",
+        item => $item_1,
+        patron => $patron,
+        result => 1,
+    },
+    {
+        msg => "Diff item, same bib, same patron [1]",
+        item => $item_2,
+        patron => $patron,
+        result => 1,
+    },
+    {
+        msg => "Diff item, diff bib, same patron [0]",
+        item => $item_d,
+        patron => $patron,
+        result => 0,
+    },
+    {
+        msg => "Same item, diff patron [0]",
+        item => $item_1,
+        patron => $patron_d,
+        result => 0,
+    },
+    {
+        msg => "Diff item, same bib, diff patron [0]",
+        item => $item_2,
+        patron => $patron_d,
+        result => 0,
+    },
+    {
+        msg => "Diff item, diff bib, diff patron [0]",
+        item => $item_d,
+        patron => $patron_d,
+        result => 0,
+    },
+];
+
+test_it($cpvPmappings, "PostIssue");
+
+# Return item_1 from patron:
+BAIL_OUT("Return Failed") unless AddReturn($item_1->{barcode}, $patron->{branchcode});
+
+# Then:
+test_it($cpvPmappings, "PostReturn");
+
+# Finally test C4::Circulation::CanBookBeIssued
+
+# We have already tested ->wantsCheckPrevCheckout and ->doCheckPrevCheckout,
+# so all that remains to be tested is whetherthe different combinational
+# outcomes of the above return values in CanBookBeIssued result in the
+# approriate $needsconfirmation.
+
+# We want to test:
+# - DESCRIPTION [RETURNVALUE (0/1)]
+# - patron, !wantsCheckPrevCheckout, !doCheckPrevCheckout
+#   [!$issuingimpossible,!$needsconfirmation->{PREVISSUE}]
+# - patron, wantsCheckPrevCheckout, !doCheckPrevCheckout
+#   [!$issuingimpossible,!$needsconfirmation->{PREVISSUE}]
+# - patron, !wantsCheckPrevCheckout, doCheckPrevCheckout
+#   [!$issuingimpossible,!$needsconfirmation->{PREVISSUE}]
+# - patron, wantsCheckPrevCheckout, doCheckPrevCheckout
+#   [!$issuingimpossible,$needsconfirmation->{PREVISSUE}]
+
+# Needs:
+# - $patron_from_GetMember
+# - $item objects (one not issued, another prevIssued)
+# - $checkprevcheckout pref (first hardno, then hardyes)
+
+# Our Patron
+my $CBBI_patron = $builder->build({source => 'Borrower'});
+my $p_from_GetMember =
+    GetMember(%{{borrowernumber => $CBBI_patron->{borrowernumber}}});
+# Our Items
+my $new_item = $builder->build({
+    source => 'Item',
+    value => {
+        notforloan => 0,
+        withdrawn  => 0,
+        itemlost   => 0,
+    },
+});
+my $prev_item = $builder->build({
+    source => 'Item',
+    value => {
+        notforloan => 0,
+        withdrawn  => 0,
+        itemlost   => 0,
+    },
+});
+# Second is Checked Out
+BAIL_OUT("CanBookBeIssued Issue failed")
+    unless AddIssue($p_from_GetMember, $prev_item->{barcode});
+
+# Mappings
+my $CBBI_mappings = [
+    {
+        syspref => 'hardno',
+        item    => $new_item,
+        result  => undef,
+        msg     => "patron, !wantsCheckPrevCheckout, !doCheckPrevCheckout"
+
+    },
+    {
+        syspref => 'hardyes',
+        item    => $new_item,
+        result  => undef,
+        msg     => "patron, wantsCheckPrevCheckout, !doCheckPrevCheckout"
+    },
+    {
+        syspref => 'hardno',
+        item    => $prev_item,
+        result  => undef,
+        msg     => "patron, !wantsCheckPrevCheckout, doCheckPrevCheckout"
+    },
+    {
+        syspref => 'hardyes',
+        item    => $prev_item,
+        result  => 1,
+        msg     => "patron, wantsCheckPrevCheckout, doCheckPrevCheckout"
+    },
+];
+
+# Tests
+map {
+    t::lib::Mocks::mock_preference('checkprevcheckout', $_->{syspref});
+    my ( $issuingimpossible, $needsconfirmation ) =
+        C4::Circulation::CanBookBeIssued(
+            $p_from_GetMember, $_->{item}->{barcode}
+        );
+    is($needsconfirmation->{PREVISSUE}, $_->{result}, $_->{msg});
+} @{$CBBI_mappings};
+
+$schema->storage->txn_rollback;
+
+1;
-- 
1.7.10.4