diff --git a/Koha/Calendar.pm b/Koha/Calendar.pm index 1ecf52cc67..b82b1f5f3a 100644 --- a/Koha/Calendar.pm +++ b/Koha/Calendar.pm @@ -484,6 +484,38 @@ sub delete_exception { return $self; } +sub closed_dates_in_range { + my ( $self, $from_dt, $to_dt ) = @_; + + croak 'Missing from_dt in closed_dates_in_range' unless $from_dt; + croak 'Missing to_dt in closed_dates_in_range' unless $to_dt; + + my $holidays = $self->_holidays; + + my @closed; + my $current = $from_dt->clone; + while ( $current <= $to_dt ) { + my $ymd = $current->ymd(''); + + # Exception (open override) wins over any other closure rule. + if ( defined $holidays->{$ymd} && $holidays->{$ymd} == 0 ) { + $current->add( days => 1 ); + next; + } + + if ( ( defined $holidays->{$ymd} && $holidays->{$ymd} == 1 ) + || $self->{weekly_closed_days}->[ $current->day_of_week % 7 ] + || $self->{day_month_closed_days}->{ $current->month }->{ $current->day } ) + { + push @closed, $current->ymd; + } + + $current->add( days => 1 ); + } + + return \@closed; +} + sub copy_to { my ( $self, $target_branchcode ) = @_; croak "No target_branchcode" unless $target_branchcode; @@ -771,6 +803,17 @@ list of specified dates $calendar->delete_exception({ date => '2026-12-25' }); +=head3 closed_dates_in_range + + my $dates = $calendar->closed_dates_in_range($from_dt, $to_dt); + +Returns an arrayref of ISO date strings (YYYY-MM-DD) representing every +closed day between C<$from_dt> and C<$to_dt> (inclusive), taking +weekly, annual-repeating, single and exception (open override) +closures into account. Exceptions win: a date listed as an exception +is never returned even if a weekly or repeating rule would otherwise +close it. + =head3 copy_to $calendar->copy_to($target_branchcode); diff --git a/t/db_dependent/Koha/Calendar.t b/t/db_dependent/Koha/Calendar.t index 7ab73c8efd..e0157638b0 100755 --- a/t/db_dependent/Koha/Calendar.t +++ b/t/db_dependent/Koha/Calendar.t @@ -19,7 +19,7 @@ use Modern::Perl; -use Test::More tests => 5; +use Test::More tests => 6; use Test::Exception; use Test::NoWarnings; @@ -229,6 +229,49 @@ subtest 'CRUD methods' => sub { $schema->storage->txn_rollback; }; +subtest 'closed_dates_in_range' => sub { + + plan tests => 5; + + $schema->storage->txn_begin; + + my $library = $builder->build_object( { class => 'Koha::Libraries' } ); + my $branchcode = $library->branchcode; + + # Sunday closed every week, 25 December every year, 2026-06-15 single, + # 2026-12-25 open exception (overrides the repeating rule). + $builder->build_object( + { class => 'Koha::Calendar::WeeklyClosures', value => { library_id => $branchcode, weekday => 0 } } ); + $builder->build_object( + { + class => 'Koha::Calendar::RepeatingClosures', + value => { library_id => $branchcode, day => 25, month => 12 } + } + ); + $builder->build_object( + { class => 'Koha::Calendar::SingleClosures', value => { library_id => $branchcode, date => '2026-06-15' } } ); + $builder->build_object( + { class => 'Koha::Calendar::Exceptions', value => { library_id => $branchcode, date => '2026-12-25' } } ); + + my $calendar = Koha::Calendar->new( branchcode => $branchcode ); + + my $closed = $calendar->closed_dates_in_range( dt_from_string('2026-06-14'), dt_from_string('2026-06-16') ); + is_deeply( $closed, [ '2026-06-14', '2026-06-15' ], 'Sunday and single closure returned' ); + + $closed = $calendar->closed_dates_in_range( dt_from_string('2026-12-24'), dt_from_string('2026-12-26') ); + is_deeply( $closed, [], 'Exception suppresses repeating closure on 2026-12-25' ); + + $closed = $calendar->closed_dates_in_range( dt_from_string('2027-06-15'), dt_from_string('2027-06-15') ); + is_deeply( $closed, [], 'Open Tuesday not returned' ); + + $closed = $calendar->closed_dates_in_range( dt_from_string('2027-12-25'), dt_from_string('2027-12-25') ); + is_deeply( $closed, ['2027-12-25'], 'Repeating closure returned in a year with no exception' ); + + throws_ok { $calendar->closed_dates_in_range() } qr/Missing from_dt/, 'croaks without from_dt'; + + $schema->storage->txn_rollback; +}; + subtest 'delete_*_closure clears _holidays cache' => sub { plan tests => 4;