Bug 6796: Consider library hours when calculating due date + tests
[koha.git] / t / db_dependent / Circulation / CalcDateDue.t
1 #!/usr/bin/perl
2
3 use Modern::Perl;
4
5 use Test::More tests => 23;
6 use Test::MockModule;
7 use DBI;
8 use DateTime;
9 use t::lib::Mocks;
10 use t::lib::TestBuilder;
11 use C4::Calendar qw( new insert_single_holiday delete_holiday insert_week_day_holiday );
12 use Koha::DateUtils qw( dt_from_string );
13 use Koha::Library::Hours;
14 use Koha::CirculationRules;
15
16 use_ok('C4::Circulation', qw( CalcDateDue ));
17
18 my $schema = Koha::Database->new->schema;
19 $schema->storage->txn_begin;
20 my $builder = t::lib::TestBuilder->new;
21
22 t::lib::Mocks::mock_preference( 'ConsiderLibraryHoursInCirculation', 'ignore' );
23 my $library = $builder->build_object({ class => 'Koha::Libraries' })->store;
24 my $dateexpiry = '2013-01-01';
25 my $patron_category = $builder->build_object({ class => 'Koha::Patron::Categories', value => { category_type => 'B' } })->store;
26 my $borrower = $builder->build_object(
27     {
28         class  => 'Koha::Patrons',
29         value  => {
30             categorycode => $patron_category->categorycode,
31             dateexpiry => $dateexpiry,
32         }
33     }
34 )->store;
35
36 my $itemtype = $builder->build_object({ class => 'Koha::ItemTypes' })->store->itemtype;
37 my $issuelength = 10;
38 my $renewalperiod = 5;
39 my $lengthunit = 'days';
40
41 Koha::CirculationRules->search()->delete();
42 Koha::CirculationRules->set_rules(
43     {
44         categorycode => $patron_category->categorycode,
45         itemtype     => $itemtype,
46         branchcode   => $library->branchcode,
47         rules        => {
48             issuelength   => $issuelength,
49             renewalperiod => $renewalperiod,
50             lengthunit    => $lengthunit,
51         }
52     }
53 );
54
55 #Set syspref ReturnBeforeExpiry = 1 and useDaysMode = 'Days'
56 t::lib::Mocks::mock_preference('ReturnBeforeExpiry', 1);
57 t::lib::Mocks::mock_preference('useDaysMode', 'Days');
58
59 my $branchcode = $library->branchcode;
60
61 my $cache = Koha::Caches->get_instance();
62 my $key   = $branchcode . "_holidays";
63 $cache->clear_from_cache($key);
64
65 my $start_date = DateTime->new({year => 2013, month => 2, day => 9});
66 my $date = C4::Circulation::CalcDateDue( $start_date, $itemtype, $branchcode, $borrower );
67 is($date, $dateexpiry . 'T23:59:00', 'date expiry');
68 $date = C4::Circulation::CalcDateDue( $start_date, $itemtype, $branchcode, $borrower, 1 );
69
70
71 #Set syspref ReturnBeforeExpiry = 1 and useDaysMode != 'Days'
72 t::lib::Mocks::mock_preference('ReturnBeforeExpiry', 1);
73 t::lib::Mocks::mock_preference('useDaysMode', 'noDays');
74
75 $start_date = DateTime->new({year => 2013, month => 2, day => 9});
76 $date = C4::Circulation::CalcDateDue( $start_date, $itemtype, $branchcode, $borrower );
77 is($date, $dateexpiry . 'T23:59:00', 'date expiry with useDaysMode to noDays');
78
79 # Let's add a special holiday on 2013-01-01. With ReturnBeforeExpiry and
80 # useDaysMode different from 'Days', return should forward the dateexpiry.
81 my $calendar = C4::Calendar->new(branchcode => $branchcode);
82 $calendar->insert_single_holiday(
83     day             => 1,
84     month           => 1,
85     year            => 2013,
86     title           =>'holidayTest',
87     description     => 'holidayDesc'
88 );
89 $date = C4::Circulation::CalcDateDue( $start_date, $itemtype, $branchcode, $borrower );
90 is($date, '2012-12-31T23:59:00', 'date expiry should be 2013-01-01 -1 day');
91 $calendar->insert_single_holiday(
92     day             => 31,
93     month           => 12,
94     year            => 2012,
95     title           =>'holidayTest',
96     description     => 'holidayDesc'
97 );
98 $date = C4::Circulation::CalcDateDue( $start_date, $itemtype, $branchcode, $borrower );
99 is($date, '2012-12-30T23:59:00', 'date expiry should be 2013-01-01 -2 day');
100
101
102 $date = C4::Circulation::CalcDateDue( $start_date, $itemtype, $branchcode, $borrower, 1 );
103
104
105 #Set syspref ReturnBeforeExpiry = 0 and useDaysMode = 'Days'
106 t::lib::Mocks::mock_preference('ReturnBeforeExpiry', 0);
107 t::lib::Mocks::mock_preference('useDaysMode', 'Days');
108
109 $start_date = DateTime->new({year => 2013, month => 2, day => 9});
110 $date = C4::Circulation::CalcDateDue( $start_date, $itemtype, $branchcode, $borrower );
111 is($date, '2013-02-' . (9 + $issuelength) . 'T23:59:00', "date expiry ( 9 + $issuelength )");
112
113 $date = C4::Circulation::CalcDateDue( $start_date, $itemtype, $branchcode, $borrower, 1 );
114 is($date, '2013-02-' . (9 + $renewalperiod) . 'T23:59:00', "date expiry ( 9 + $renewalperiod )");
115
116
117 # Now we want to test the Dayweek useDaysMode option
118 # For this we need a loan period that is a mutiple of 7 days
119 # But, since we currently don't have that, let's test it does the
120 # right thing in that case, it should act as though useDaysMode is set to
121 # Datedue
122 #Set syspref ReturnBeforeExpiry = 0 and useDaysMode = 'Dayweek'
123 t::lib::Mocks::mock_preference('ReturnBeforeExpiry', 0);
124 t::lib::Mocks::mock_preference('useDaysMode', 'Dayweek');
125
126 # No closed day interfering, so we should get the regular due date
127 $date = C4::Circulation::CalcDateDue( $start_date, $itemtype, $branchcode, $borrower );
128 is($date, '2013-02-' . (9 + $issuelength) . 'T23:59:00', "useDaysMode = Dayweek, no closed days, issue date expiry ( start + $issuelength )");
129
130 $date = C4::Circulation::CalcDateDue( $start_date, $itemtype, $branchcode, $borrower, 1 );
131 is($date, '2013-02-' . (9 + $renewalperiod) . 'T23:59:00', "useDaysMode = Dayweek, no closed days, renewal date expiry ( start + $renewalperiod )");
132
133 # Now let's add a closed day on the expected renewal date, it should
134 # roll forward as per Datedue (i.e. one day at a time)
135 # For issues...
136 $calendar->insert_single_holiday(
137     day             => 9 + $issuelength,
138     month           => 2,
139     year            => 2013,
140     title           =>'DayweekTest1',
141     description     => 'DayweekTest1'
142 );
143 $date = C4::Circulation::CalcDateDue( $start_date, $itemtype, $branchcode, $borrower );
144 is($date, '2013-02-' . (9 + $issuelength + 1) . 'T23:59:00', "useDaysMode = Dayweek, closed on due date, 10 day loan (should not trigger 7 day roll forward), issue date expiry ( start + $issuelength  + 1 )");
145 # Remove the holiday we just created
146 $calendar->delete_holiday(
147     day             => 9 + $issuelength,
148     month           => 2,
149     year            => 2013
150 );
151
152 # ...and for renewals...
153 $calendar->insert_single_holiday(
154     day             => 9 + $renewalperiod,
155     month           => 2,
156     year            => 2013,
157     title           =>'DayweekTest2',
158     description     => 'DayweekTest2'
159 );
160 $date = C4::Circulation::CalcDateDue( $start_date, $itemtype, $branchcode, $borrower, 1 );
161 is($date, '2013-02-' . (9 + $renewalperiod + 1) . 'T23:59:00', "useDaysMode = Dayweek, closed on due date, 5 day renewal (should not trigger 7 day roll forward), renewal date expiry ( start + $renewalperiod  + 1 )");
162 # Remove the holiday we just created
163 $calendar->delete_holiday(
164     day             => 9 + $renewalperiod,
165     month           => 2,
166     year            => 2013,
167 );
168
169 # Now we test it does the right thing if the loan and renewal periods
170 # are a multiple of 7 days
171 my $dayweek_issuelength = 14;
172 my $dayweek_renewalperiod = 7;
173 my $dayweek_lengthunit = 'days';
174
175 $patron_category = $builder->build_object({ class => 'Koha::Patron::Categories', value => { category_type => 'K' } })->store;
176
177 Koha::CirculationRules->set_rules(
178     {
179         categorycode => $patron_category->categorycode,
180         itemtype     => $itemtype,
181         branchcode   => $branchcode,
182         rules        => {
183             issuelength   => $dayweek_issuelength,
184             renewalperiod => $dayweek_renewalperiod,
185             lengthunit    => $dayweek_lengthunit,
186         }
187     }
188 );
189
190 my $dayweek_borrower = $builder->build_object(
191     {
192         class  => 'Koha::Patrons',
193         value  => {
194             categorycode => $patron_category->categorycode,
195             dateexpiry => $dateexpiry,
196         }
197     }
198 );
199
200 # For issues...
201 $start_date = DateTime->new({year => 2013, month => 2, day => 9});
202 $calendar->insert_single_holiday(
203     day             => 9 + $dayweek_issuelength,
204     month           => 2,
205     year            => 2013,
206     title           =>'DayweekTest3',
207     description     => 'DayweekTest3'
208 );
209 $date = C4::Circulation::CalcDateDue( $start_date, $itemtype, $branchcode, $dayweek_borrower );
210 my $issue_should_add = $dayweek_issuelength + 7;
211 my $dayweek_issue_expected = $start_date->add( days => $issue_should_add );
212 is($date, $dayweek_issue_expected->strftime('%F') . 'T23:59:00', "useDaysMode = Dayweek, closed on due date, 14 day loan (should trigger 7 day roll forward), issue date expiry ( start + $issue_should_add )");
213 # Remove the holiday we just created
214 $calendar->delete_holiday(
215     day             => 9 + $dayweek_issuelength,
216     month           => 2,
217     year            => 2013,
218 );
219
220 # ...and for renewals...
221 $start_date = DateTime->new({year => 2013, month => 2, day => 9});
222 $calendar->insert_single_holiday(
223     day             => 9 + $dayweek_renewalperiod,
224     month           => 2,
225     year            => 2013,
226     title           => 'DayweekTest4',
227     description     => 'DayweekTest4'
228 );
229 $date = C4::Circulation::CalcDateDue( $start_date, $itemtype, $branchcode, $dayweek_borrower, 1 );
230 my $renewal_should_add = $dayweek_renewalperiod + 7;
231 my $dayweek_renewal_expected = $start_date->add( days => $renewal_should_add );
232 is($date, $dayweek_renewal_expected->strftime('%F') . 'T23:59:00', "useDaysMode = Dayweek, closed on due date, 7 day renewal (should trigger 7 day roll forward), renewal date expiry ( start + $renewal_should_add )");
233 # Remove the holiday we just created
234 $calendar->delete_holiday(
235     day             => 9 + $dayweek_renewalperiod,
236     month           => 2,
237     year            => 2013,
238 );
239
240 # Now test it continues to roll forward by 7 days until it finds
241 # an open day, so we create a 3 week period of closed Saturdays
242 $start_date = DateTime->new({year => 2013, month => 2, day => 9});
243 my $expected_rolled_date = DateTime->new({year => 2013, month => 3, day => 9});
244 my $holiday = $start_date->clone();
245 $holiday->add(days => 7);
246 $calendar->insert_single_holiday(
247     day             => $holiday->day,
248     month           => $holiday->month,
249     year            => 2013,
250     title           =>'DayweekTest5',
251     description     => 'DayweekTest5'
252 );
253 $holiday->add(days => 7);
254 $calendar->insert_single_holiday(
255     day             => $holiday->day,
256     month           => $holiday->month,
257     year            => 2013,
258     title           =>'DayweekTest6',
259     description     => 'DayweekTest6'
260 );
261 $holiday->add(days => 7);
262 $calendar->insert_single_holiday(
263     day             => $holiday->day,
264     month           => $holiday->month,
265     year            => 2013,
266     title           =>'DayweekTest7',
267     description     => 'DayweekTest7'
268 );
269 # For issues...
270 $date = C4::Circulation::CalcDateDue( $start_date, $itemtype, $branchcode, $dayweek_borrower );
271 $dayweek_issue_expected = $start_date->add( days => $issue_should_add );
272 is($date, $expected_rolled_date->strftime('%F') . 'T23:59:00', "useDaysMode = Dayweek, closed on due date and two subequent due dates, 14 day loan (should trigger 2 x 7 day roll forward), issue date expiry ( start + 28 )");
273 # ...and for renewals...
274 $start_date = DateTime->new({year => 2013, month => 2, day => 9});
275 $date = C4::Circulation::CalcDateDue( $start_date, $itemtype, $branchcode, $dayweek_borrower, 1 );
276 $dayweek_issue_expected = $start_date->add( days => $renewal_should_add );
277 is($date, $expected_rolled_date->strftime('%F') . 'T23:59:00', "useDaysMode = Dayweek, closed on due date and three subsequent due dates, 7 day renewal (should trigger 3 x 7 day roll forward), issue date expiry ( start + 28 )");
278 # Remove the holidays we just created
279 $start_date = DateTime->new({year => 2013, month => 2, day => 9});
280 my $del_holiday = $start_date->clone();
281 $del_holiday->add(days => 7);
282 $calendar->delete_holiday(
283     day             => $del_holiday->day,
284     month           => $del_holiday->month,
285     year            => 2013
286 );
287 $del_holiday->add(days => 7);
288 $calendar->delete_holiday(
289     day             => $del_holiday->day,
290     month           => $del_holiday->month,
291     year            => 2013
292 );
293 $del_holiday->add(days => 7);
294 $calendar->delete_holiday(
295     day             => $del_holiday->day,
296     month           => $del_holiday->month,
297     year            => 2013
298 );
299
300 # Now test that useDaysMode "Dayweek" doesn't try to roll forward onto
301 # a permanently closed day and instead rolls forward just one day
302 $start_date = DateTime->new({year => 2013, month => 2, day => 9});
303 # Our tests are concerned with Saturdays, so let's close on Saturdays
304 $calendar->insert_week_day_holiday(
305     weekday => 6,
306     title => "Saturday closure",
307     description => "Closed on Saturdays"
308 );
309 # For issues...
310 $date = C4::Circulation::CalcDateDue( $start_date, $itemtype, $branchcode, $dayweek_borrower );
311 $dayweek_issue_expected = $start_date->add( days => $dayweek_issuelength + 1 );
312 is($date, $dayweek_issue_expected->strftime('%F') . 'T23:59:00', "useDaysMode = Dayweek, due on Saturday, closed on Saturdays, 14 day loan (should trigger 1 day roll forward), issue date expiry ( start + 15 )");
313 # ...and for renewals...
314 $start_date = DateTime->new({year => 2013, month => 2, day => 9});
315 $date = C4::Circulation::CalcDateDue( $start_date, $itemtype, $branchcode, $dayweek_borrower, 1 );
316 $dayweek_renewal_expected = $start_date->add( days => $dayweek_renewalperiod + 1 );
317 is($date, $dayweek_renewal_expected->strftime('%F') . 'T23:59:00', "useDaysMode = Dayweek, due on Saturday, closed on Saturdays, 7 day renewal (should trigger 1 day roll forward), issue date expiry ( start + 8 )");
318 # Remove the holiday we just created
319 $calendar->delete_holiday(
320     weekday => 6
321 );
322
323 # Renewal period of 0 is valid
324 Koha::CirculationRules->search()->delete();
325 Koha::CirculationRules->set_rules(
326     {
327         categorycode => undef,
328         itemtype     => undef,
329         branchcode   => undef,
330         rules        => {
331             issuelength   => 9999,
332             renewalperiod => 0,
333             lengthunit    => 'days',
334             daysmode      => 'Days',
335         }
336     }
337 );
338 $date = C4::Circulation::CalcDateDue( $start_date, $itemtype, $branchcode, $borrower, 1 );
339 is( $date->ymd, $start_date->ymd, "Dates should match for renewalperiod of 0" );
340
341 # Renewal period of "" should trigger fallover to issuelength for renewal
342 Koha::CirculationRules->search()->delete();
343 Koha::CirculationRules->set_rules(
344     {
345         categorycode => undef,
346         itemtype     => undef,
347         branchcode   => undef,
348         rules        => {
349             issuelength   => 7,
350             renewalperiod => q{},
351             lengthunit    => 'days',
352             daysmode      => 'Days',
353         }
354     }
355 );
356 my $renewed_date = $start_date->clone->add( days => 7 );
357 $date = C4::Circulation::CalcDateDue( $start_date, $itemtype, $branchcode, $borrower, 1 );
358 is( $date->ymd, $renewed_date->ymd, 'Renewal period of "" should trigger fallover to issuelength for renewal' );
359
360 # Testing hourly loans consider library open hours
361
362 my $library1 = $builder->build( { source => 'Branch' } );
363 Koha::CirculationRules->set_rules(
364     {
365         categorycode => $categorycode,
366         itemtype     => $itemtype,
367         branchcode   => $library1->{branchcode},
368         rules        => {
369             issuelength   => 3, # loan period is 3 hours
370             lengthunit    => 'hours',
371         }
372     }
373 );
374
375 my $open = DateTime->now->subtract( hours => 4 )->hms;
376 my $close = DateTime->now->add( hours => 2 )->hms;
377 my $now = DateTime->now;
378
379 foreach (0..6) {
380     # library opened 4 hours ago and closes in 2 hours.
381     Koha::Library::Hour->new({ day => $_, library_id => $library1->{branchcode}, open_time => $open, close_time => $close })->store;
382 }
383
384 # ignore calendar
385 t::lib::Mocks::mock_preference('useDaysMode', 'Days');
386 t::lib::Mocks::mock_preference('ConsiderLibraryHoursInCirculation', 'close');
387 # shorten loan period
388
389 $date = C4::Circulation::CalcDateDue( $now, $itemtype, $library1->{branchcode}, $borrower );
390 my $expected_duetime = DateTime->now->add( hours => 2 );
391 is( $date, $expected_duetime, "Loan period was shortened because ConsiderLibraryHoursInCirculation is set to close time" );
392
393 t::lib::Mocks::mock_preference('ConsiderLibraryHoursWhenIssuing', 'open');
394 # extend loan period
395
396 $date = C4::Circulation::CalcDateDue( $now, $itemtype, $library1->{branchcode}, $borrower );
397 $expected_duetime = DateTime->now->add( days => 1 )->subtract( hours => 4 );
398 is( $date, $expected_duetime, "Loan period was extended because ConsiderLibraryHoursInCirculation is set to open time" );
399
400 my $holiday_tomorrow = DateTime->now->add( days => 1 );
401
402 # consider calendar
403 my $library1_calendar = C4::Calendar->new( branchcode => $library1->{branchcode} );
404 $library1_calendar->insert_single_holiday(
405     day             => $holiday_tomorrow->day,
406     month           => $holiday_tomorrow->month,
407     year            => $holiday_tomorrow->year,
408     title           => 'testholiday',
409     description     => 'testholiday'
410 );
411 Koha::CirculationRules->set_rules(
412     {
413         categorycode => $categorycode,
414         itemtype     => $itemtype,
415         branchcode   => $library1->{branchcode},
416         rules        => {
417             issuelength   => 13, # loan period must cross over into tomorrow
418             lengthunit    => 'hours',
419         }
420     }
421 );
422
423 t::lib::Mocks::mock_preference('useDaysMode', 'Calendar');
424 t::lib::Mocks::mock_preference('ConsiderLibraryHoursInCirculation', 'close');
425 # shorten loan period
426
427 $date = C4::Circulation::CalcDateDue( $now, $itemtype, $library1->{branchcode}, $borrower );
428 $expected_duetime = DateTime->now->add( days => 2, hours => 2 );
429 is( $date, $expected_duetime, "Loan period was shortened (but considers the holiday) because ConsiderLibraryHoursInCirculation is set to close time" );
430
431 t::lib::Mocks::mock_preference( 'ConsiderLibraryHoursInCirculation', 'open' );
432 # extend loan period
433
434 $date = C4::Circulation::CalcDateDue( $now, $itemtype, $library1->{branchcode}, $borrower );
435 $expected_duetime = DateTime->now->add( days => 2 )->subtract( hours => 4 );
436 is( $date, $expected_duetime, "Loan period was extended (but considers the holiday) because ConsiderLibraryHoursInCirculation is set to open time" );
437
438 $cache->clear_from_cache($key);
439 $schema->storage->txn_rollback;