From 1ed36ede36d41e350a20da83a6f29d909c0ef9ac Mon Sep 17 00:00:00 2001
From: Emmi Takkinen <emmi.takkinen@koha-suomi.fi>
Date: Fri, 17 Feb 2023 13:15:17 +0200
Subject: [PATCH] Bug 12133: Add requirements for guarantor and guarantee

Add two requirements when registering a new patron:

- A child patron must have a guarantor. This is controlled by
  a new syspref ChildNeedsGuarantor.
- A guarantor cannot be a guarantee.

Test plan:
1. Add a child patron without guarantor or child patron with guarantee
as guarantor succesfully.
2. Apply this patch.
3. Add a child patron as a guarantor.
=> Error is raised.
4. Turn syspref "ChildNeedsGuarantor" ON.
5. Add a child patron without a guarantor and error "Child needs a
guarantor" is raised.
6. Add guarantor. Guarantor can either be existing patron or added with
"Contact" section.
=> Save without errors.

Also prove t/db_dependent/Koha/Patron.t

Sponsored-by: Koha-Suomi Oy
---
 Koha/Exceptions/Patron/Relationship.pm        | 11 +++-
 Koha/Patron.pm                                | 20 +++++-
 .../data/mysql/atomicupdate/bug_12133.pl      | 17 +++++
 installer/data/mysql/mandatory/sysprefs.sql   |  1 +
 .../en/modules/admin/preferences/patrons.pref |  7 ++
 .../prog/en/modules/members/memberentrygen.tt |  6 ++
 members/memberentry.pl                        | 32 ++++++++-
 t/db_dependent/Koha/Patron.t                  | 66 ++++++++++++++++++-
 8 files changed, 154 insertions(+), 6 deletions(-)
 create mode 100755 installer/data/mysql/atomicupdate/bug_12133.pl

diff --git a/Koha/Exceptions/Patron/Relationship.pm b/Koha/Exceptions/Patron/Relationship.pm
index 616ad830c7..6a0cc5dd76 100644
--- a/Koha/Exceptions/Patron/Relationship.pm
+++ b/Koha/Exceptions/Patron/Relationship.pm
@@ -32,8 +32,12 @@ use Exception::Class (
     'Koha::Exceptions::Patron::Relationship::InvalidRelationship' => {
         isa         => 'Koha::Exceptions::Patron::Relationship',
         description => 'The specified relationship is invalid',
-        fields      =>  ['relationship','no_relationship']
-    }
+        fields => [ 'relationship', 'no_relationship', 'invalid_guarantor' ]
+    },
+    'Koha::Exceptions::Patron::Relationship::NoGuarantor' => {
+        isa         => 'Koha::Exceptions::Patron::Relationship',
+        description => 'Child patron needs a guarantor',
+    },
 );
 
 sub full_message {
@@ -46,6 +50,9 @@ sub full_message {
             if ( $self->no_relationship ) {
                 $msg = sprintf( "No relationship passed." );
             }
+            elsif ( $self->invalid_guarantor ) {
+                $msg = sprintf("Child patron cannot be a guarantor.");
+            }
             else {
                 $msg = sprintf("Invalid relationship passed, '%s' is not defined.", $self->relationship );
             }
diff --git a/Koha/Patron.pm b/Koha/Patron.pm
index ec54d76360..487d0d46e3 100644
--- a/Koha/Patron.pm
+++ b/Koha/Patron.pm
@@ -185,7 +185,10 @@ to db
 =cut
 
 sub store {
-    my ($self) = @_;
+    my $self = shift;
+    my $params = @_ ? shift : {};
+
+    my $guarantors = $params->{guarantors} // [];
 
     $self->_result->result_source->schema->txn_do(
         sub {
@@ -222,6 +225,21 @@ sub store {
               if defined $self->relationship
                      and $self->relationship eq "";
 
+            if ( C4::Context->preference('ChildNeedsGuarantor')
+                and $self->is_child
+                and $self->contactname eq ""
+                and !@$guarantors )
+            {
+                Koha::Exceptions::Patron::Relationship::NoGuarantor->throw();
+            }
+
+            foreach my $guarantor (@$guarantors) {
+                if ( $guarantor->is_child ) {
+                    Koha::Exceptions::Patron::Relationship::InvalidRelationship
+                      ->throw( invalid_guarantor => 1 );
+                }
+            }
+
             unless ( $self->in_storage ) {    #AddMember
 
                 # Generate a valid userid/login if needed
diff --git a/installer/data/mysql/atomicupdate/bug_12133.pl b/installer/data/mysql/atomicupdate/bug_12133.pl
new file mode 100755
index 0000000000..e426f787eb
--- /dev/null
+++ b/installer/data/mysql/atomicupdate/bug_12133.pl
@@ -0,0 +1,17 @@
+use Modern::Perl;
+
+return {
+    bug_number => "12133",
+    description => "Add system preference ChildNeedsGuarantor",
+    up => sub {
+        my ($args) = @_;
+        my ($dbh, $out) = @$args{qw(dbh out)};
+
+        $dbh->do( q{
+            INSERT IGNORE INTO systempreferences (variable,value,explanation,options,type)
+            VALUES('ChildNeedsGuarantor', 0, 'If ON, a child patron must have a guarantor when adding the patron.', '', 'YesNo');
+        } );
+
+        say $out "Added new system preference 'ChildNeedsGuarantor')\n";
+    },
+};
diff --git a/installer/data/mysql/mandatory/sysprefs.sql b/installer/data/mysql/mandatory/sysprefs.sql
index beebd27a6f..90ce7cd946 100644
--- a/installer/data/mysql/mandatory/sysprefs.sql
+++ b/installer/data/mysql/mandatory/sysprefs.sql
@@ -136,6 +136,7 @@ INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, `
 ('ChargeFinesOnClosedDays','0',NULL,'Charge fines on days the library is closed.','YesNo'),
 ('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'),
 ('CheckPrevCheckoutDelay','0', NULL,'Maximum number of days that will trigger a warning if the patron has borrowed that item in the past when CheckPrevCheckout is enabled.','free'),
+('ChildNeedsGuarantor', 0, 'If ON, a child patron must have a guarantor when adding the patron.', '', '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'),
 ('CircConfirmItemParts', '0', NULL, 'Require staff to confirm that all parts of an item are present at checkin/checkout.', 'Yes/No'),
 ('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/preferences/patrons.pref b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/patrons.pref
index 9ea2bea534..c6a3c7eb54 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
@@ -348,6 +348,13 @@ Patrons:
                1: Allow
                0: "Don't allow"
          - staff to set the ability for a patron's fines to be viewed by linked patrons in the OPAC.
+     -
+         - "A child patron"
+         - pref: "ChildNeedsGuarantor"
+           choices:
+               yes: "must have"
+               no: "doesn't need"
+         - a guarantor when adding the patron.
 
     Privacy:
      -
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 365b15600c..d5753318e5 100644
--- a/koha-tmpl/intranet-tmpl/prog/en/modules/members/memberentrygen.tt
+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/members/memberentrygen.tt
@@ -195,6 +195,12 @@ legend:hover {
                                     [% IF ERROR_bad_email_alternative %]
                                         <li id="ERROR_bad_email_alternative">The alternative email is invalid.</li>
                                     [% END %]
+                                    [% IF ( ERROR_child_no_guarantor ) %]
+                                        <li id="ERROR_child_no_guarantor">A child patron needs a guarantor.</li>
+                                    [% END %]
+                                    [% IF ( ERROR_guarantor_is_guarantee ) %]
+                                        <li id="ERROR_guarantor_is_guarantee">A guarantor cannot be a guarantee.</li>
+                                    [% END %]
                                 </ul>
                             </div>
                         [% END %]
diff --git a/members/memberentry.pl b/members/memberentry.pl
index 0193cc3777..e94e1b82b5 100755
--- a/members/memberentry.pl
+++ b/members/memberentry.pl
@@ -105,6 +105,18 @@ my @relations = split /\|/, C4::Context->preference('borrowerRelationship'), -1;
 my $empty_relationship_allowed = grep {$_ eq ""} @relations;
 $template->param( empty_relationship_allowed => $empty_relationship_allowed );
 
+#Search existing guarantor id(s) and new ones from params
+my @guarantors;
+my @new_guarantor_ids = grep { $_ ne '' } $input->multi_param('new_guarantor_id');
+
+foreach my $new_guarantor_id (@new_guarantor_ids) {
+    my $new_guarantor = Koha::Patrons->find( { borrowernumber => $new_guarantor_id } );
+    push @guarantors, $new_guarantor;
+}
+
+my @existing_guarantors = $patron->guarantor_relationships()->guarantors->as_list unless !$patron;
+push @guarantors, @existing_guarantors;
+
 my $guarantor_id = $input->param('guarantor_id');
 my $guarantor = undef;
 $guarantor = Koha::Patrons->find( $guarantor_id ) if $guarantor_id;
@@ -265,6 +277,22 @@ if ( ( $op eq 'insert' ) and !$nodouble ) {
     }
 }
 
+#Check if guarantor requirements are met
+my $valid_guarantor = @guarantors ? @guarantors : $newdata{'contactname'};
+if ( C4::Context->preference('ChildNeedsGuarantor')
+    && $category->category_type eq 'C'
+    && !$valid_guarantor
+    && ( $op eq 'save' || $op eq 'insert' ) )
+{
+    push @errors, 'ERROR_child_no_guarantor';
+}
+
+foreach my $guarantor (@guarantors) {
+    if ( $guarantor->is_child && ( $op eq 'save' || $op eq 'insert' ) ) {
+        push @errors, 'ERROR_guarantor_is_guarantee';
+    }
+}
+
 ###############test to take the right zipcode, country and city name ##############
 # set only if parameter was passed from the form
 $newdata{'city'}    = $input->param('city')    if defined($input->param('city'));
@@ -428,7 +456,7 @@ if ((!$nok) and $nodouble and ($op eq 'insert' or $op eq 'save')){
 	if ($op eq 'insert'){
 		# we know it's not a duplicate borrowernumber or there would already be an error
         delete $newdata{password2};
-        $patron = eval { Koha::Patron->new(\%newdata)->store };
+        $patron = eval { Koha::Patron->new(\%newdata)->store({ guarantors => \@guarantors }) };
         if ( $@ ) {
             # FIXME Urgent error handling here, we cannot fail without relevant feedback
             # Lot of code will need to be removed from this script to handle exceptions raised by Koha::Patron->store
@@ -514,7 +542,7 @@ if ((!$nok) and $nodouble and ($op eq 'insert' or $op eq 'save')){
         delete $newdata{password2};
 
         eval {
-            $patron->set(\%newdata)->store if scalar(keys %newdata) > 1; # bug 4508 - avoid crash if we're not
+            $patron->set(\%newdata)->store({ guarantors => \@guarantors }) if scalar(keys %newdata) > 1; # bug 4508 - avoid crash if we're not
                                                                     # updating any columns in the borrowers table,
                                                                     # which can happen if we're only editing the
                                                                     # patron attributes or messaging preferences sections
diff --git a/t/db_dependent/Koha/Patron.t b/t/db_dependent/Koha/Patron.t
index e92b054d86..466404428e 100755
--- a/t/db_dependent/Koha/Patron.t
+++ b/t/db_dependent/Koha/Patron.t
@@ -19,7 +19,7 @@
 
 use Modern::Perl;
 
-use Test::More tests => 19;
+use Test::More tests => 20;
 use Test::Exception;
 use Test::Warn;
 
@@ -1349,3 +1349,67 @@ subtest 'notify_library_of_registration()' => sub {
 
     $schema->storage->txn_rollback;
 };
+
+subtest 'guarantor requirements tests' => sub {
+
+    plan tests => 3;
+
+    $schema->storage->txn_begin;
+
+    my $branchcode = $builder->build( { source => 'Branch' } )->{branchcode};
+    my $child_category = $builder->build(
+        { source => 'Category', value => { category_type => 'C' } } )
+      ->{categorycode};
+    my $patron_category = $builder->build(
+        { source => 'Category', value => { category_type => 'A' } } )
+      ->{categorycode};
+
+    t::lib::Mocks::mock_preference( 'ChildNeedsGuarantor', 0 );
+
+    my $child = Koha::Patron->new(
+        {
+            branchcode   => $branchcode,
+            categorycode => $child_category,
+            contactname  => ''
+        }
+    );
+    $child->store();
+
+    ok(
+        Koha::Patrons->find( $child->id ),
+'Child patron can be stored without guarantor when ChildNeedsGuarantor is off.'
+    );
+
+    t::lib::Mocks::mock_preference( 'ChildNeedsGuarantor', 1 );
+
+    my $child2 = Koha::Patron->new(
+        {
+            branchcode   => $branchcode,
+            categorycode => $child_category,
+            contactname  => ''
+        }
+    );
+    my $child3 = $builder->build_object(
+        {
+            class => 'Koha::Patrons',
+            value => { categorycode => $child_category }
+        }
+    );
+    my $patron = $builder->build_object(
+        {
+            class => 'Koha::Patrons',
+            value => { categorycode => $patron_category }
+        }
+    );
+
+    throws_ok { $child2->store(); }
+    'Koha::Exceptions::Patron::Relationship::NoGuarantor',
+      'Exception thrown when guarantor is required but not provided.';
+
+    my @guarantors = ( $patron, $child3 );
+    throws_ok { $child2->store( { guarantors => \@guarantors } ); }
+    'Koha::Exceptions::Patron::Relationship::InvalidRelationship',
+      'Exception thrown when child patron is added as guarantor.';
+
+    $schema->storage->txn_rollback;
+};
-- 
2.34.1