Bug 21431: Add unit test
[koha.git] / t / db_dependent / Koha / Checkouts.t
1 #!/usr/bin/perl
2
3 # Copyright 2015 Koha Development team
4 #
5 # This file is part of Koha
6 #
7 # Koha is free software; you can redistribute it and/or modify it
8 # under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
11 #
12 # Koha is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License
18 # along with Koha; if not, see <http://www.gnu.org/licenses>.
19
20 use Modern::Perl;
21
22 use Test::More tests => 12;
23 use Test::MockModule;
24 use Test::Warn;
25
26 use C4::Circulation qw( MarkIssueReturned AddReturn );
27 use C4::Reserves qw( AddReserve );
28 use Koha::Checkouts;
29 use Koha::Database;
30 use Koha::DateUtils qw( dt_from_string );
31 use Koha::Holds;
32
33 use t::lib::TestBuilder;
34 use t::lib::Mocks;
35
36 my $schema = Koha::Database->new->schema;
37 $schema->storage->txn_begin;
38
39 my $builder = t::lib::TestBuilder->new;
40 my $library = $builder->build( { source => 'Branch' } );
41 my $patron  = $builder->build(
42     { source => 'Borrower', value => { branchcode => $library->{branchcode} } }
43 );
44 my $item_1          = $builder->build_sample_item;
45 my $item_2          = $builder->build_sample_item;
46 my $nb_of_checkouts = Koha::Checkouts->search->count;
47 my $new_checkout_1  = Koha::Checkout->new(
48     {
49         borrowernumber => $patron->{borrowernumber},
50         itemnumber     => $item_1->itemnumber,
51         branchcode     => $library->{branchcode},
52     }
53 )->store;
54 my $new_checkout_2 = Koha::Checkout->new(
55     {
56         borrowernumber => $patron->{borrowernumber},
57         itemnumber     => $item_2->itemnumber,
58         branchcode     => $library->{branchcode},
59     }
60 )->store;
61
62 like( $new_checkout_1->issue_id, qr|^\d+$|,
63     'Adding a new checkout should have set the issue_id' );
64 is(
65     Koha::Checkouts->search->count,
66     $nb_of_checkouts + 2,
67     'The 2 checkouts should have been added'
68 );
69
70 my $retrieved_checkout_1 = Koha::Checkouts->find( $new_checkout_1->issue_id );
71 is(
72     $retrieved_checkout_1->itemnumber,
73     $new_checkout_1->itemnumber,
74     'Find a checkout by id should return the correct checkout'
75 );
76
77 subtest 'is_overdue' => sub {
78     plan tests => 6;
79     my $ten_days_ago   = dt_from_string->add( days => -10 );
80     my $ten_days_later = dt_from_string->add( days => 10 );
81     my $yesterday      = dt_from_string->add( days => -1 );
82     my $tomorrow       = dt_from_string->add( days => 1 );
83
84     $retrieved_checkout_1->date_due($ten_days_ago)->store;
85     is( $retrieved_checkout_1->is_overdue,
86         1, 'The item should have been returned 10 days ago' );
87
88     $retrieved_checkout_1->date_due($ten_days_later)->store;
89     is( $retrieved_checkout_1->is_overdue, 0, 'The item is due in 10 days' );
90
91     $retrieved_checkout_1->date_due($tomorrow)->store;
92     is( $retrieved_checkout_1->is_overdue($ten_days_later),
93         1, 'The item should have been returned yesterday' );
94
95     $retrieved_checkout_1->date_due($yesterday)->store;
96     is( $retrieved_checkout_1->is_overdue($ten_days_ago),
97         0, 'Ten days ago the item due yesterday was not late' );
98
99     $retrieved_checkout_1->date_due($tomorrow)->store;
100     is( $retrieved_checkout_1->is_overdue($ten_days_later),
101         1, 'In Ten days, the item due tomorrow will be late' );
102
103     $retrieved_checkout_1->date_due($yesterday)->store;
104     is( $retrieved_checkout_1->is_overdue($ten_days_ago),
105         0, 'In Ten days, the item due yesterday will still be late' );
106 };
107
108 subtest 'item' => sub {
109     plan tests => 2;
110     my $item = $retrieved_checkout_1->item;
111     is( ref($item), 'Koha::Item',
112         'Koha::Checkout->item should return a Koha::Item' );
113     is( $item->itemnumber, $item_1->itemnumber,
114         'Koha::Checkout->item should return the correct item' );
115 };
116
117 subtest 'account_lines' => sub {
118     plan tests => 3;
119
120     my $accountline = Koha::Account::Line->new(
121         {
122             issue_id          => $retrieved_checkout_1->id,
123             borrowernumber    => $retrieved_checkout_1->borrowernumber,
124             itemnumber        => $retrieved_checkout_1->itemnumber,
125             branchcode        => $retrieved_checkout_1->branchcode,
126             date              => \'NOW()',
127             debit_type_code   => 'OVERDUE',
128             status            => 'UNRETURNED',
129             interface         => 'cli',
130             amount            => '1',
131             amountoutstanding => '1',
132         }
133     )->store();
134
135     my $account_lines = $retrieved_checkout_1->account_lines;
136     is( ref($account_lines), 'Koha::Account::Lines',
137         'Koha::Checkout->account_lines should return a Koha::Account::Lines' );
138
139     my $line = $account_lines->next;
140     is( ref($line), 'Koha::Account::Line',
141         'next returns a Koha::Account::Line' );
142
143     is(
144         $accountline->id,
145         $line->id,
146         'Koha::Checkout->account_lines should return the correct account_lines'
147     );
148 };
149
150 subtest 'patron' => sub {
151     plan tests => 3;
152     my $patron = $builder->build_object(
153         {
154             class => 'Koha::Patrons',
155             value => { branchcode => $library->{branchcode} }
156         }
157     );
158
159     my $item     = $builder->build_sample_item;
160     my $checkout = Koha::Checkout->new(
161         {
162             borrowernumber => $patron->borrowernumber,
163             itemnumber     => $item->itemnumber,
164             branchcode     => $library->{branchcode},
165         }
166     )->store;
167
168     my $p = $checkout->patron;
169     is( ref($p), 'Koha::Patron',
170         'Koha::Checkout->patron should return a Koha::Patron' );
171     is( $p->borrowernumber, $patron->borrowernumber,
172         'Koha::Checkout->patron should return the correct patron' );
173
174     # Testing Koha::Old::Checkout->patron now
175     my $issue_id = $checkout->issue_id;
176     C4::Circulation::MarkIssueReturned( $p->borrowernumber,
177         $checkout->itemnumber );
178     $p->delete;
179     my $old_issue = Koha::Old::Checkouts->find($issue_id);
180     is( $old_issue->patron, undef,
181 'Koha::Checkout->patron should return undef if the patron record has been deleted'
182     );
183 };
184
185 $retrieved_checkout_1->delete;
186 is(
187     Koha::Checkouts->search->count,
188     $nb_of_checkouts + 1,
189     'Delete should have deleted the checkout'
190 );
191
192 subtest 'issuer' => sub {
193     plan tests => 3;
194     my $patron = $builder->build_object(
195         {
196             class => 'Koha::Patrons',
197             value => { branchcode => $library->{branchcode} }
198         }
199     );
200     my $issuer = $builder->build_object(
201         {
202             class => 'Koha::Patrons',
203             value => { branchcode => $library->{branchcode} }
204         }
205     );
206
207     my $item     = $builder->build_sample_item;
208     my $checkout = Koha::Checkout->new(
209         {
210             borrowernumber => $patron->borrowernumber,
211             issuer_id      => $issuer->borrowernumber,
212             itemnumber     => $item->itemnumber,
213             branchcode     => $library->{branchcode},
214         }
215     )->store;
216
217     my $i = $checkout->issuer;
218     is( ref($i), 'Koha::Patron',
219         'Koha::Checkout->issuer should return a Koha::Patron' );
220     is( $i->borrowernumber, $issuer->borrowernumber,
221         'Koha::Checkout->issuer should return the correct patron' );
222
223     # Testing Koha::Old::Checkout->patron now
224     my $issue_id = $checkout->issue_id;
225     C4::Circulation::MarkIssueReturned( $patron->borrowernumber,
226         $checkout->itemnumber );
227     $i->delete;
228     my $old_issue = Koha::Old::Checkouts->find($issue_id);
229     is( $old_issue->issuer_id, undef,
230 'Koha::Checkout->issuer_id should return undef if the patron record has been deleted'
231     );
232
233 };
234
235 subtest 'Koha::Old::Checkouts->filter_by_todays_checkins' => sub {
236
237     plan tests => 3;
238
239     # We will create 7 checkins for a given patron
240     # 3 checked in today - 2 days, and 4 checked in today
241     my $librarian = $builder->build_object(
242         {
243             class => 'Koha::Patrons',
244             value => { branchcode => $library->{branchcode} }
245         }
246     );
247     t::lib::Mocks::mock_userenv( { patron => $librarian } );
248     my $patron = $builder->build_object(
249         {
250             class => 'Koha::Patrons',
251             value => { branchcode => $library->{branchcode} }
252         }
253     );
254
255     my @checkouts;
256     # Create 7 checkouts
257     for ( 0 .. 6 ) {
258         my $item = $builder->build_sample_item;
259         push @checkouts,
260           Koha::Checkout->new(
261             {
262                 borrowernumber => $patron->borrowernumber,
263                 itemnumber     => $item->itemnumber,
264                 branchcode     => $library->{branchcode},
265             }
266         )->store;
267     }
268
269     # Checkin 3 today - 2 days
270     my $not_today = dt_from_string->add( days => -2 );
271     for my $i ( 0 .. 2 ) {
272         my $checkout = $checkouts[$i];
273         C4::Circulation::AddReturn(
274             $checkout->item->barcode, $library->{branchcode},
275             undef, $not_today->set_hour( int( rand(24) ) )
276         );
277     }
278     # Checkin 4 today
279     my $today = dt_from_string;
280     for my $i ( 3 .. 6 ) {
281         my $checkout = $checkouts[$i];
282         C4::Circulation::AddReturn(
283             $checkout->item->barcode, $library->{branchcode},
284             undef, $today->set_hour( int( rand(24) ) )
285         );
286     }
287
288     my $old_checkouts = $patron->old_checkouts;
289     is( $old_checkouts->count, 7, 'There should be 7 old checkouts' );
290     my $todays_checkins = $old_checkouts->filter_by_todays_checkins;
291     is( $todays_checkins->count, 4, 'There should be 4 checkins today' );
292     is_deeply(
293         [ $todays_checkins->get_column('itemnumber') ],
294         [ map { $_->itemnumber } @checkouts[ 3 .. 6 ] ],
295         q{Correct list of today's checkins}
296     );
297 };
298
299 $schema->storage->txn_rollback;
300
301 subtest 'automatic_checkin' => sub {
302
303     plan tests => 10;
304
305     $schema->storage->txn_begin;
306
307     my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
308
309     my $due_ac_item =
310       $builder->build_sample_item(
311         { homebranch => $patron->branchcode, itemlost => 0 } );
312     my $ac_item =
313       $builder->build_sample_item(
314         { homebranch => $patron->branchcode, itemlost => 0 } );
315     my $odue_ac_item =
316       $builder->build_sample_item(
317         { homebranch => $patron->branchcode, itemlost => 0 } );
318     my $normal_item =
319       $builder->build_sample_item(
320         { homebranch => $patron->branchcode, itemlost => 0 } );
321
322     $due_ac_item->itemtype->automatic_checkin(1)->store;
323     $odue_ac_item->itemtype->automatic_checkin(1)->store;
324     $ac_item->itemtype->automatic_checkin(1)->store;
325     $normal_item->itemtype->automatic_checkin(0)->store;
326
327     my $today     = dt_from_string;
328     my $tomorrow  = dt_from_string->add( days => 1 );
329     my $yesterday = dt_from_string->subtract( days => 1 );
330
331     # Checkout do for automatic checkin
332     my $checkout_due_aci = Koha::Checkout->new(
333         {
334             borrowernumber => $patron->borrowernumber,
335             itemnumber     => $due_ac_item->itemnumber,
336             branchcode     => $patron->branchcode,
337             date_due       => $today,
338         }
339     )->store;
340
341     # Checkout not due for automatic checkin
342     my $checkout_odue_aci = Koha::Checkout->new(
343         {
344             borrowernumber => $patron->borrowernumber,
345             itemnumber     => $odue_ac_item->itemnumber,
346             branchcode     => $patron->branchcode,
347             date_due       => $yesterday
348         }
349     )->store;
350
351     # Checkout not due for automatic checkin
352     my $checkout_aci = Koha::Checkout->new(
353         {
354             borrowernumber => $patron->borrowernumber,
355             itemnumber     => $ac_item->itemnumber,
356             branchcode     => $patron->branchcode,
357             date_due       => $tomorrow
358         }
359     )->store;
360
361     # due checkout for nomal itemtype
362     my $checkout_ni = Koha::Checkout->new(
363         {
364             borrowernumber => $patron->borrowernumber,
365             itemnumber     => $normal_item->itemnumber,
366             branchcode     => $patron->branchcode,
367             date_due       => $today,
368         }
369     )->store;
370
371     my $searched = Koha::Checkouts->find( $checkout_ni->issue_id );
372     is( $searched->issue_id, $checkout_ni->issue_id,
373         'checkout for normal_item exists' );
374
375     $searched = Koha::Checkouts->find( $checkout_aci->issue_id );
376     is( $searched->issue_id, $checkout_aci->issue_id,
377         'checkout for ac_item exists' );
378
379     $searched = Koha::Checkouts->find( $checkout_due_aci->issue_id );
380     is(
381         $searched->issue_id,
382         $checkout_due_aci->issue_id,
383         'checkout for due_ac_item exists'
384     );
385
386     $searched = Koha::Checkouts->find( $checkout_odue_aci->issue_id );
387     is(
388         $searched->issue_id,
389         $checkout_odue_aci->issue_id,
390         'checkout for odue_ac_item exists'
391     );
392
393     Koha::Checkouts->automatic_checkin;
394
395     $searched = Koha::Checkouts->find( $checkout_ni->issue_id );
396     is( $searched->issue_id, $checkout_ni->issue_id,
397         'checkout for normal_item still exists' );
398
399     $searched = Koha::Checkouts->find( $checkout_aci->issue_id );
400     is( $searched->issue_id, $checkout_aci->issue_id,
401         'checkout for ac_item still exists' );
402
403     $searched = Koha::Checkouts->find( $checkout_due_aci->issue_id );
404     is( $searched, undef, 'checkout for due_ac_item doesn\'t exist anymore' );
405
406     $searched = Koha::Checkouts->find( $checkout_odue_aci->issue_id );
407     is( $searched, undef, 'checkout for odue_ac_item doesn\'t exist anymore' );
408
409     $searched = Koha::Old::Checkouts->find( $checkout_odue_aci->issue_id );
410     is( dt_from_string($searched->returndate), $yesterday, 'old checkout for odue_ac_item has the right return date' );
411
412
413     subtest 'automatic_checkin AutomaticCheckinAutoFill tests' => sub {
414
415         plan tests => 3;
416
417         my $checkout_2_due_ac = Koha::Checkout->new(
418             {
419                 borrowernumber => $patron->borrowernumber,
420                 itemnumber     => $due_ac_item->itemnumber,
421                 branchcode     => $patron->branchcode,
422                 date_due       => $today
423             }
424         )->store;
425
426         my $patron_2 =
427             $builder->build_object( { class => 'Koha::Patrons', value => { branchcode => $patron->branchcode } } );
428         my $reserveid = AddReserve(
429             {
430                 branchcode     => $patron->branchcode,
431                 borrowernumber => $patron_2->id,
432                 biblionumber   => $due_ac_item->biblionumber,
433                 priority       => 1
434             }
435         );
436
437         t::lib::Mocks::mock_preference( 'AutomaticCheckinAutoFill', '0' );
438
439         Koha::Checkouts->automatic_checkin;
440         my $reserve = Koha::Holds->find($reserveid);
441
442         is( $reserve->found, undef, "Hold was not filled when AutomaticCheckinAutoFill disabled" );
443
444         my $checkout_3_due_ac = Koha::Checkout->new(
445             {
446                 borrowernumber => $patron->borrowernumber,
447                 itemnumber     => $due_ac_item->itemnumber,
448                 branchcode     => $patron->branchcode,
449                 date_due       => $today
450             }
451         )->store;
452         t::lib::Mocks::mock_preference( 'AutomaticCheckinAutoFill', '1' );
453
454         Koha::Checkouts->automatic_checkin;
455         $reserve->discard_changes;
456
457         is( $reserve->found, 'W', "Hold was filled when AutomaticCheckinAutoFill enabled" );
458
459         my $checkout_2_odue_ac = Koha::Checkout->new(
460             {
461                 borrowernumber => $patron->borrowernumber,
462                 itemnumber     => $odue_ac_item->itemnumber,
463                 branchcode     => $patron->branchcode,
464                 date_due       => $today
465             }
466         )->store;
467         my $branch2    = $builder->build_object( { class => "Koha::Libraries" } );
468         my $reserve2id = AddReserve(
469             {
470                 branchcode     => $branch2->branchcode,
471                 borrowernumber => $patron_2->id,
472                 biblionumber   => $odue_ac_item->biblionumber,
473                 priority       => 1
474             }
475         );
476         Koha::Checkouts->automatic_checkin;
477
478         my $reserve2 = Koha::Holds->find($reserve2id);
479         is(
480             $reserve2->found, 'T',
481             "Hold was filled when AutomaticCheckinAutoFill enabled and transfer was initiated when branches didn't match"
482         );
483     };
484
485     $schema->storage->txn_rollback;
486 };
487
488 subtest 'attempt_auto_renew' => sub {
489
490     plan tests => 33;
491
492     $schema->storage->txn_begin;
493
494     my $renew_error = 'auto_renew';
495     my $module      = Test::MockModule->new('C4::Circulation');
496     $module->mock( 'CanBookBeRenewed', sub { return ( 1, $renew_error ) } );
497     $module->mock( 'AddRenewal',       sub { warn "AddRenewal called" } );
498     my $checkout = $builder->build_object(
499         {
500             class => 'Koha::Checkouts',
501             value => {
502                 date_due         => '2023-01-01 23:59:59',
503                 returndate       => undef,
504                 auto_renew       => 1,
505                 auto_renew_error => undef,
506                 onsite_checkout  => 0,
507                 renewals_count   => 0,
508             }
509         }
510     );
511
512     my ( $success, $error, $updated );
513     warning_is {
514         ( $success, $error, $updated ) = $checkout->attempt_auto_renew();
515     }
516     undef, "AddRenewal not called without confirm";
517     ok( $success, "Issue is renewed when error is 'auto_renew'" );
518     is( $error, undef, "No error when renewed" );
519     ok( $updated, "Issue reported as updated when renewed" );
520
521     warning_is {
522         ( $success, $error, $updated ) = $checkout->attempt_auto_renew( { confirm => 1 } );
523     }
524     "AddRenewal called", "AddRenewal called when confirm is passed";
525     ok( $success, "Issue is renewed when error is 'auto_renew'" );
526     is( $error, undef, "No error when renewed" );
527     ok( $updated, "Issue reported as updated when renewed" );
528
529     $module->mock( 'AddRenewal', sub { return; } );
530
531     $renew_error = 'anything_else';
532     ( $success, $error, $updated ) = $checkout->attempt_auto_renew();
533     ok( !$success, "Success is untrue for any other status" );
534     is( $error, 'anything_else', "The error is passed through" );
535     ok( $updated, "Issue reported as updated when status changes" );
536     $checkout->discard_changes();
537     is( $checkout->auto_renew_error, undef, "Error not updated if confirm not passed" );
538
539     ( $success, $error, $updated ) = $checkout->attempt_auto_renew( { confirm => 1 } );
540     ok( !$success, "Success is untrue for any other status" );
541     is( $error, 'anything_else', "The error is passed through" );
542     ok( $updated, "Issue updated when confirm passed" );
543     $checkout->discard_changes();
544     is( $checkout->auto_renew_error, 'anything_else', "Error updated if confirm passed" );
545
546     # Error now equals 'anything_else'
547     ( $success, $error, $updated ) = $checkout->attempt_auto_renew();
548     ok( !$updated, "Issue not reported as updated when status has not changed" );
549
550     $renew_error = "auto_unseen_final";
551     ( $success, $error, $updated ) = $checkout->attempt_auto_renew( { confirm => 1 } );
552     ok( $success, "Issue is renewed when error is 'auto_unseen_final'" );
553     is( $error, 'auto_unseen_final', "Error of finality reported when renewed" );
554     ok( $updated, "Issue reported as updated when renewed" );
555     $checkout->discard_changes();
556     is( $checkout->auto_renew_error, 'auto_unseen_final', "Error updated" );
557
558     $renew_error = "too_unseen";
559     ( $success, $error, $updated ) = $checkout->attempt_auto_renew( { confirm => 1 } );
560     ok( !$success, "Issue is not renewed when error is 'too_unseen'" );
561     is( $error, 'too_unseen', "Error reported correctly" );
562     ok( !$updated, "Issue not reported as updated when moved from final to too unseen" );
563     $checkout->discard_changes();
564     is( $checkout->auto_renew_error, 'too_unseen', "Error updated" );
565
566     $renew_error = "auto_renew_final";
567     ( $success, $error, $updated ) = $checkout->attempt_auto_renew( { confirm => 1 } );
568     ok( $success, "Issue is renewed when error is 'auto_renew_final'" );
569     is( $error, 'auto_renew_final', "Error of finality reported when renewed" );
570     ok( $updated, "Issue reported as updated when renewed" );
571     $checkout->discard_changes();
572     is( $checkout->auto_renew_error, 'auto_renew_final', "Error updated" );
573
574     $renew_error = "too_many";
575     ( $success, $error, $updated ) = $checkout->attempt_auto_renew( { confirm => 1 } );
576     ok( !$success, "Issue is not renewed when error is 'too_many'" );
577     is( $error, 'too_many', "Error reported correctly" );
578     ok( !$updated, "Issue not reported as updated when moved from final to too many" );
579     $checkout->discard_changes();
580     is( $checkout->auto_renew_error, 'too_many', "Error updated" );
581
582     $schema->storage->txn_rollback;
583 };