From 1dfb8701636ee565893ab55119083e147e3219eb Mon Sep 17 00:00:00 2001
From: Agustin Moyano <agustinmoyano@theke.io>
Date: Mon, 16 Sep 2019 17:47:43 -0300
Subject: [PATCH] Bug 23260: Add anonymize items_last_borrower feature

This patch adds the feature to anonymize patrons from items_last_borrower table. This code is run from batch_anonymize script, which is triggered from a cron job.

To test:
1) apply this patch and the previous one
2) perl installer/data/mysql/updatedatabase.pl
3) set StoreLastBorrower preference to Allow.
4) Create a Check out followed by a Check in.
CHECK => a row should appear in items_last_borrower table with the borrower and the item number.
5) In mysql, update created_on date to one day earlier.
6) set AnonymousPatron preference to a valid patron id
7) perl misc/cronjobs/batch_anonymise.pl
SUCCESS => borrower number in items_last_borrower changed to AnonymousPatron id
8) repeat steps 4 and 5
9) set AnonymousPatron preference to 0
10) perl misc/cronjobs/batch_anonymise.pl
SUCCESS => borrower number in items_last_borrower changed to null
11) prove t/db_dependent/Koha/Patrons.t
12) Sign off

Signed-off-by: Maryse Simard <maryse.simard@inlibro.com>
---
 Koha/Item/LastPatron.pm          |  44 +++++++++++++
 Koha/Item/LastPatrons.pm         |  54 +++++++++++++++
 Koha/Patrons.pm                  |  87 ++++++++++++++++++++----
 misc/cronjobs/batch_anonymise.pl |   5 +-
 t/db_dependent/Koha/Patrons.t    | 139 ++++++++++++++++++++++++++++++++++++++-
 5 files changed, 315 insertions(+), 14 deletions(-)
 create mode 100644 Koha/Item/LastPatron.pm
 create mode 100644 Koha/Item/LastPatrons.pm

diff --git a/Koha/Item/LastPatron.pm b/Koha/Item/LastPatron.pm
new file mode 100644
index 0000000..501b34d
--- /dev/null
+++ b/Koha/Item/LastPatron.pm
@@ -0,0 +1,44 @@
+package Koha::Item::LastPatron;
+
+# This file is part of Koha.
+#
+# Koha is free software; you can redistribute it and/or modify it under the
+# terms of the GNU General Public License as published by the Free Software
+# Foundation; either version 3 of the License, or (at your option) any later
+# version.
+#
+# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with Koha; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+use Modern::Perl;
+
+use Carp;
+
+use Koha::Database;
+
+use base qw(Koha::Object);
+
+=head1 NAME
+
+Koha::Item::LastPatron - Koha Item LastPatron Object class
+
+=head1 API
+
+=head2 Class Methods
+
+=cut
+
+=head3 type
+
+=cut
+
+sub _type {
+    return 'ItemsLastBorrower';
+}
+
+1;
diff --git a/Koha/Item/LastPatrons.pm b/Koha/Item/LastPatrons.pm
new file mode 100644
index 0000000..3a91b92
--- /dev/null
+++ b/Koha/Item/LastPatrons.pm
@@ -0,0 +1,54 @@
+package Koha::Item::LastPatrons;
+
+# This file is part of Koha.
+#
+# Koha is free software; you can redistribute it and/or modify it under the
+# terms of the GNU General Public License as published by the Free Software
+# Foundation; either version 3 of the License, or (at your option) any later
+# version.
+#
+# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with Koha; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+use Modern::Perl;
+
+use Carp;
+
+use Koha::Item::LastPatron;
+
+use Koha::Database;
+
+use base qw(Koha::Objects);
+
+=head1 NAME
+
+Koha::Item::LastPatrons - Koha Item LastPatron Object set class
+
+=head1 API
+
+=head2 Class Methods
+
+=cut
+
+=head3 type
+
+=cut
+
+sub _type {
+    return 'ItemsLastBorrower';
+}
+
+=head3 object_class
+
+=cut
+
+sub object_class {
+    return 'Koha::Item::LastPatron';
+}
+
+1;
diff --git a/Koha/Patrons.pm b/Koha/Patrons.pm
index 4a5f63c..6c030c9 100644
--- a/Koha/Patrons.pm
+++ b/Koha/Patrons.pm
@@ -29,6 +29,7 @@ use Koha::ArticleRequests;
 use Koha::ArticleRequest::Status;
 use Koha::Patron;
 use Koha::Exceptions::Patron;
+use Koha::Item::LastPatrons;
 
 use base qw(Koha::Objects);
 
@@ -127,7 +128,7 @@ sub search_upcoming_membership_expires {
 
 =head3 search_patrons_to_anonymise
 
-    my $patrons = Koha::Patrons->search_patrons_to_anonymise( { before => $older_than_date, [ library => $library ] } );
+    my $patrons = Koha::Patrons->search_patrons_to_anonymise( { before => $older_than_date, [ library => $library, for => $table_to_anonymize ] } );
 
 This method returns all patrons who has an issue history older than a given date.
 
@@ -137,6 +138,7 @@ sub search_patrons_to_anonymise {
     my ( $class, $params ) = @_;
     my $older_than_date = $params->{before};
     my $library         = $params->{library};
+    my $for           = $params->{for};
     $older_than_date = $older_than_date ? dt_from_string($older_than_date) : dt_from_string;
     $library ||=
       ( C4::Context->preference('IndependentBranches') && C4::Context->userenv && !C4::Context->IsSuperLibrarian() && C4::Context->userenv->{branch} )
@@ -145,17 +147,35 @@ sub search_patrons_to_anonymise {
     my $anonymous_patron = C4::Context->preference('AnonymousPatron') || undef;
 
     my $dtf = Koha::Database->new->schema->storage->datetime_parser;
-    my $rs = $class->_resultset->search(
-        {   returndate                  => { '<'   =>  $dtf->format_datetime($older_than_date), },
-            'old_issues.borrowernumber' => { 'not' => undef },
-            privacy                     => { '<>'  => 0 },                  # Keep forever
-            ( $library ? ( 'old_issues.branchcode' => $library ) : () ),
-            ( $anonymous_patron ? ( 'old_issues.borrowernumber' => { '!=' => $anonymous_patron } ) : () ),
-        },
-        {   join     => ["old_issues"],
-            distinct => 1,
-        }
-    );
+    my $rs;
+    if($for && $for eq 'items_last_borrower') {
+        $rs = $class->_resultset->search(
+            {   'items_last_borrowers.created_on' => { '<'   =>  $dtf->format_datetime($older_than_date), },
+                'items_last_borrowers.borrowernumber' => { 'not' => undef },
+                'itemnumber.damaged' => 0,
+                'itemnumber.itemlost' => 0,
+                'itemnumber.withdrawn' => 0,
+                ( $library ? ( 'itemnumber.homebranch' => $library ) : () ),
+                ( $anonymous_patron ? ( 'items_last_borrowers.borrowernumber' => { '!=' => $anonymous_patron } ) : () ),
+            },
+            {   join     => { "items_last_borrowers" => "itemnumber" },
+                distinct => 1,
+            }
+        );
+    } else {
+        #for old_issues
+        $rs = $class->_resultset->search(
+            {   returndate                  => { '<'   =>  $dtf->format_datetime($older_than_date), },
+                'old_issues.borrowernumber' => { 'not' => undef },
+                privacy                     => { '<>'  => 0 },                  # Keep forever
+                ( $library ? ( 'old_issues.branchcode' => $library ) : () ),
+                ( $anonymous_patron ? ( 'old_issues.borrowernumber' => { '!=' => $anonymous_patron } ) : () ),
+            },
+            {   join     => ["old_issues"],
+                distinct => 1,
+            }
+        );
+    }
     return Koha::Patrons->_new_from_dbic($rs);
 }
 
@@ -198,6 +218,49 @@ sub anonymise_issue_history {
     return $nb_rows;
 }
 
+=head3 anonymise_items_last_borrower
+
+    Koha::Patrons->search->anonymise_items_last_borrower( { [ before => $older_than_date ] } );
+
+Anonymise last borrower history (items_last_borrower) for all patrons older than the given date (optional).
+To make sure all the conditions are met, the caller has the responsibility to
+call search_patrons_to_anonymise to filter the Koha::Patrons set
+
+=cut
+
+sub anonymise_items_last_borrower {
+    my ( $self, $params ) = @_;
+
+    my $older_than_date = $params->{before};
+
+    $older_than_date = dt_from_string $older_than_date if $older_than_date;
+
+    # The default of 0 does not work due to foreign key constraints
+    # The anonymisation should not fail quietly if AnonymousPatron is not a valid entry
+    # Set it to undef (NULL)
+    my $dtf = Koha::Database->new->schema->storage->datetime_parser;
+    my $nb_rows = 0;
+    while ( my $patron = $self->next ) {
+        my $last_borrowers = Koha::Item::LastPatrons->search(
+        {
+            borrowernumber => $patron->borrowernumber,
+            (
+                $older_than_date
+                ? ( created_on =>
+                      { '<' => $dtf->format_datetime($older_than_date) } )
+                : ()
+            )
+        }
+        );
+        my $anonymous_patron = C4::Context->preference('AnonymousPatron') || undef;
+        while(my $last_borrower = $last_borrowers->next) {
+            $last_borrower->borrowernumber( $anonymous_patron )->store;
+            $nb_rows++;
+        }
+    }
+    return $nb_rows;
+}
+
 =head3 delete
 
     Koha::Patrons->search({ some filters here })->delete({ move => 1, verbose => 1 });
diff --git a/misc/cronjobs/batch_anonymise.pl b/misc/cronjobs/batch_anonymise.pl
index ea63dfa..e031fc1 100755
--- a/misc/cronjobs/batch_anonymise.pl
+++ b/misc/cronjobs/batch_anonymise.pl
@@ -73,9 +73,12 @@ cronlogaction();
 my ($year,$month,$day) = Today();
 my ($newyear,$newmonth,$newday) = Add_Delta_Days ($year,$month,$day,(-1)*$days);
 my $formatdate = sprintf "%4d-%02d-%02d",$newyear,$newmonth,$newday;
-$verbose and print "Checkouts before $formatdate will be anonymised.\n";
+$verbose and print "Checkouts and items las borrowers before $formatdate will be anonymised.\n";
 
 my $rows = Koha::Patrons->search_patrons_to_anonymise( { before => $formatdate } )->anonymise_issue_history( { before => $formatdate } );
 $verbose and print int($rows) . " checkouts anonymised.\n";
 
+$rows = Koha::Patrons->search_patrons_to_anonymise( { before => $formatdate , for => 'items_last_borrower'} )->anonymise_items_last_borrower( { before => $formatdate } );
+$verbose and print int($rows) . " items last borrower anonymised.\n";
+
 exit(0);
diff --git a/t/db_dependent/Koha/Patrons.t b/t/db_dependent/Koha/Patrons.t
index c8d0118..f5b1c18 100644
--- a/t/db_dependent/Koha/Patrons.t
+++ b/t/db_dependent/Koha/Patrons.t
@@ -19,7 +19,7 @@
 
 use Modern::Perl;
 
-use Test::More tests => 40;
+use Test::More tests => 41;
 use Test::Warn;
 use Test::Exception;
 use Test::MockModule;
@@ -40,6 +40,8 @@ use Koha::Patron::Categories;
 use Koha::Patron::Relationship;
 use Koha::Database;
 use Koha::DateUtils;
+use DateTime;
+use DateTime::Duration;
 use Koha::Virtualshelves;
 
 use t::lib::TestBuilder;
@@ -1178,6 +1180,141 @@ subtest 'search_patrons_to_anonymise & anonymise_issue_history' => sub {
     t::lib::Mocks::mock_preference('IndependentBranches', 0);
 };
 
+subtest 'search_patrons_to_anonymise & anonymise_items_last_borrower' => sub {
+    plan tests => 3;
+
+    my $branch = $builder->build({ source => 'Branch' });
+    my $userenv_patron = $builder->build_object({
+        class  => 'Koha::Patrons',
+        value  => { branchcode => $branch->{branchcode}, flags => 0 },
+    });
+    t::lib::Mocks::mock_userenv({ patron => $userenv_patron });
+    t::lib::Mocks::mock_preference('StoreLastBorrower', 1);
+
+    subtest 'AnonymousPatron is defined' => sub {
+        plan tests => 3;
+
+        my $anonymous = $builder->build( { source => 'Borrower', }, );
+        t::lib::Mocks::mock_preference( 'AnonymousPatron', $anonymous->{borrowernumber} );
+        t::lib::Mocks::mock_preference('IndependentBranches', 0);
+        my $patron = $builder->build(
+            {   source => 'Borrower',
+                value  => { privacy => 1, }
+            }
+        );
+        my $item = $builder->build(
+            {   source => 'Item',
+                value  => {
+                    itemlost  => 0,
+                    withdrawn => 0,
+                    damaged => 0
+                },
+            }
+        );
+        my $issue = $builder->build(
+            {   source => 'Issue',
+                value  => {
+                    borrowernumber => $patron->{borrowernumber},
+                    itemnumber     => $item->{itemnumber},
+                },
+            }
+        );
+
+        my $now = DateTime->now;
+        my ( $returned, undef, undef ) = C4::Circulation::AddReturn( $item->{barcode}, undef, undef, $now );
+        is( $returned, 1, 'The item should have been returned' );
+        my $before = $now + DateTime::Duration->new( days => 1 );
+        my $rows_affected = Koha::Patrons->search_patrons_to_anonymise( { before => $before->ymd, for => 'items_last_borrower' } )->anonymise_items_last_borrower( { before => $before->ymd } );
+        ok( $rows_affected > 0, 'AnonymiseIssueHistory should affect at least 1 row' );
+        my $dbh = C4::Context->dbh;
+        my ($borrowernumber_used_to_anonymised) = $dbh->selectrow_array(q|
+            SELECT borrowernumber FROM items_last_borrower where itemnumber = ?
+        |, undef, $item->{itemnumber});
+        is( $borrowernumber_used_to_anonymised, $anonymous->{borrowernumber}, 'AnonymousPatron should have been used to anonymise' );
+        Koha::Patrons->find( $patron->{borrowernumber})->delete;
+        Koha::Patrons->find( $anonymous->{borrowernumber})->delete;
+    };
+
+    subtest 'AnonymousPatron is not defined' => sub {
+        plan tests => 3;
+
+        t::lib::Mocks::mock_preference('IndependentBranches', 0);
+        t::lib::Mocks::mock_preference( 'AnonymousPatron', '' );
+        my $patron = $builder->build(
+            {   source => 'Borrower',
+                value  => { privacy => 1, }
+            }
+        );
+        my $item = $builder->build(
+            {   source => 'Item',
+                value  => {
+                    itemlost  => 0,
+                    withdrawn => 0,
+                    damaged => 0
+                },
+            }
+        );
+        my $issue = $builder->build(
+            {   source => 'Issue',
+                value  => {
+                    borrowernumber => $patron->{borrowernumber},
+                    itemnumber     => $item->{itemnumber},
+                },
+            }
+        );
+
+        my $now = DateTime->now;
+        my ( $returned, undef, undef ) = C4::Circulation::AddReturn( $item->{barcode}, undef, undef, $now );
+        is( $returned, 1, 'The item should have been returned' );
+        my $before = $now + DateTime::Duration->new( days => 1 );
+        my $rows_affected = Koha::Patrons->search_patrons_to_anonymise( { before => $before->ymd, for => 'items_last_borrower' } )->anonymise_items_last_borrower( { before => $before->ymd } );
+        ok( $rows_affected > 0, 'AnonymiseIssueHistory should affect at least 1 row' );
+        my $dbh = C4::Context->dbh;
+        my ($borrowernumber_used_to_anonymised) = $dbh->selectrow_array(q|
+            SELECT borrowernumber FROM items_last_borrower where itemnumber = ?
+        |, undef, $item->{itemnumber});
+        is( $borrowernumber_used_to_anonymised, undef, 'undef should have been used to anonymise' );
+        Koha::Patrons->find( $patron->{borrowernumber})->delete;
+    };
+
+    subtest 'Logged in librarian is not superlibrarian & IndependentBranches' => sub {
+        plan tests => 1;
+        t::lib::Mocks::mock_preference( 'IndependentBranches', 1 );
+        my $patron = $builder->build(
+            {   source => 'Borrower',
+                value  => { privacy => 1 }    # Another branchcode than the logged in librarian
+            }
+        );
+        my $item = $builder->build(
+            {   source => 'Item',
+                value  => {
+                    itemlost  => 0,
+                    withdrawn => 0,
+                    damaged => 0
+                },
+            }
+        );
+        my $issue = $builder->build(
+            {   source => 'Issue',
+                value  => {
+                    borrowernumber => $patron->{borrowernumber},
+                    itemnumber     => $item->{itemnumber},
+                },
+            }
+        );
+
+        my $now = DateTime->now;
+        my ( $returned, undef, undef ) = C4::Circulation::AddReturn( $item->{barcode}, undef, undef, $now );
+        my $before = $now + DateTime::Duration->new( days => 1 );
+        is( Koha::Patrons->search_patrons_to_anonymise( { before => $before->ymd, for => 'items_last_borrower' } )->count, 0 );
+        Koha::Patrons->find( $patron->{borrowernumber})->delete;
+    };
+
+    $userenv_patron->delete;
+    t::lib::Mocks::mock_preference('StoreLastBorrower', 0);
+    t::lib::Mocks::mock_preference('IndependentBranches', 0);
+};
+
 subtest 'libraries_where_can_see_patrons + can_see_patron_infos + search_limited' => sub {
     plan tests => 3;
 
-- 
2.7.4