diff --git a/Koha/REST/V1/Libraries.pm b/Koha/REST/V1/Libraries.pm index 544164f602..2975bc9a89 100644 --- a/Koha/REST/V1/Libraries.pm +++ b/Koha/REST/V1/Libraries.pm @@ -18,7 +18,10 @@ package Koha::REST::V1::Libraries; use Modern::Perl; use Mojo::Base 'Mojolicious::Controller'; +use C4::Context; use Koha::Libraries; +use Koha::Calendar; +use Koha::DateUtils qw( dt_from_string ); use Scalar::Util qw( blessed ); @@ -209,4 +212,54 @@ sub list_cash_registers { }; } +=head3 list_holidays + +Controller function that returns closed days for a library within a date range. +Used by booking calendar to disable selection of closed days. + +=cut + +sub list_holidays { + my $c = shift->openapi->valid_input or return; + + my $library_id = $c->param('library_id'); + my $from = $c->param('from'); + my $to = $c->param('to'); + + my $library = Koha::Libraries->find($library_id); + + return $c->render_resource_not_found("Library") + unless $library; + + return try { + my $from_dt = $from ? dt_from_string( $from, 'iso' ) : dt_from_string(); + my $to_dt = $to ? dt_from_string( $to, 'iso' ) : $from_dt->clone->add( months => 3 ); + + if ( $to_dt->compare($from_dt) < 0 ) { + return $c->render( + status => 400, + openapi => { error => "'to' date must be after 'from' date" } + ); + } + + my $calendar = Koha::Calendar->new( branchcode => $library_id ); + my @holidays; + + my $current = $from_dt->clone; + while ( $current <= $to_dt ) { + if ( $calendar->is_holiday($current) ) { + push @holidays, $current->ymd; + } + $current->add( days => 1 ); + } + + return $c->render( + status => 200, + openapi => \@holidays + ); + } catch { + $c->unhandled_exception($_); + }; +} + 1; diff --git a/api/v1/swagger/paths/libraries.yaml b/api/v1/swagger/paths/libraries.yaml index 1f06fd4cea..6c3e1fbb2a 100644 --- a/api/v1/swagger/paths/libraries.yaml +++ b/api/v1/swagger/paths/libraries.yaml @@ -425,6 +425,62 @@ x-koha-authorization: permissions: catalogue: 1 +"/libraries/{library_id}/holidays": + get: + x-mojo-to: Libraries#list_holidays + operationId: listLibraryHolidays + tags: + - libraries + summary: List holidays for a library + description: | + Returns a list of closed days (holidays) for a library within a date range. + Used by booking calendar to disable selection of closed days. + parameters: + - $ref: "../swagger.yaml#/parameters/library_id_pp" + - name: from + in: query + description: Start date for the range (ISO 8601 format, e.g., 2024-01-01). Defaults to today. + required: false + type: string + format: date + - name: to + in: query + description: End date for the range (ISO 8601 format, e.g., 2024-03-31). Defaults to 3 months from 'from'. + required: false + type: string + format: date + produces: + - application/json + responses: + "200": + description: A list of holiday dates in YYYY-MM-DD format + schema: + type: array + items: + type: string + format: date + "400": + description: Bad request + schema: + $ref: "../swagger.yaml#/definitions/error" + "404": + description: Library not found + schema: + $ref: "../swagger.yaml#/definitions/error" + "500": + description: | + Internal server error. Possible `error_code` attribute values: + + * `internal_server_error` + schema: + $ref: "../swagger.yaml#/definitions/error" + "503": + description: Under maintenance + schema: + $ref: "../swagger.yaml#/definitions/error" + x-koha-authorization: + permissions: + catalogue: "1" /public/libraries: get: x-mojo-to: Libraries#list diff --git a/api/v1/swagger/swagger.yaml b/api/v1/swagger/swagger.yaml index d8f3b4ccaa..01ea17d163 100644 --- a/api/v1/swagger/swagger.yaml +++ b/api/v1/swagger/swagger.yaml @@ -499,6 +499,8 @@ paths: $ref: "./paths/libraries.yaml#/~1libraries~1{library_id}~1cash_registers" "/libraries/{library_id}/desks": $ref: "./paths/libraries.yaml#/~1libraries~1{library_id}~1desks" + "/libraries/{library_id}/holidays": + $ref: "./paths/libraries.yaml#/~1libraries~1{library_id}~1holidays" "/oauth/login/{provider_code}/{interface}": $ref: ./paths/oauth.yaml#/~1oauth~1login~1{provider_code}~1{interface} /oauth/token: diff --git a/t/db_dependent/api/v1/libraries.t b/t/db_dependent/api/v1/libraries.t index e775a79917..35db436b26 100755 --- a/t/db_dependent/api/v1/libraries.t +++ b/t/db_dependent/api/v1/libraries.t @@ -18,7 +18,7 @@ use Modern::Perl; use Test::NoWarnings; -use Test::More tests => 8; +use Test::More tests => 9; use Test::Mojo; use Test::Warn; @@ -27,6 +27,7 @@ use t::lib::Mocks; use List::Util qw(min); +use Koha::Caches; use Koha::Libraries; use Koha::Database; @@ -438,3 +439,100 @@ subtest 'list_cash_registers() tests' => sub { $schema->storage->txn_rollback; }; + +subtest 'list_holidays() tests' => sub { + + plan tests => 16; + + $schema->storage->txn_begin; + + my $library = $builder->build_object( { class => 'Koha::Libraries' } ); + my $patron = $builder->build_object( + { + class => 'Koha::Patrons', + value => { flags => 4 } + } + ); + my $password = 'thePassword123'; + $patron->set_password( { password => $password, skip_validation => 1 } ); + my $userid = $patron->userid; + + my $branchcode = $library->branchcode; + my $cache = Koha::Caches->get_instance(); + + # Clear existing holidays for this branch + $schema->resultset('SpecialHoliday')->search( { branchcode => $branchcode } )->delete; + $schema->resultset('RepeatableHoliday')->search( { branchcode => $branchcode } )->delete; + $cache->clear_from_cache( $branchcode . '_holidays' ); + + # Add a specific holiday + $builder->build( + { + source => 'SpecialHoliday', + value => { + branchcode => $branchcode, + day => 15, + month => 6, + year => 2026, + title => 'Test holiday', + description => '', + isexception => 0, + }, + } + ); + + # Add a second holiday + $builder->build( + { + source => 'SpecialHoliday', + value => { + branchcode => $branchcode, + day => 20, + month => 6, + year => 2026, + title => 'Another holiday', + description => '', + isexception => 0, + }, + } + ); + $cache->clear_from_cache( $branchcode . '_holidays' ); + + # Returns holidays within range + $t->get_ok("//$userid:$password@/api/v1/libraries/$branchcode/holidays?from=2026-06-01&to=2026-06-30") + ->status_is(200); + + my $holidays = $t->tx->res->json; + ok( ( grep { $_ eq '2026-06-15' } @$holidays ), 'First holiday found in response' ); + ok( ( grep { $_ eq '2026-06-20' } @$holidays ), 'Second holiday found in response' ); + + # Returns empty list when no holidays in range + $t->get_ok("//$userid:$password@/api/v1/libraries/$branchcode/holidays?from=2026-07-01&to=2026-07-05") + ->status_is(200); + + my $no_holidays = $t->tx->res->json; + my @filtered = grep { $_ eq '2026-06-15' || $_ eq '2026-06-20' } @$no_holidays; + is( scalar @filtered, 0, 'No test holidays outside their range' ); + + # 400 when to < from + $t->get_ok("//$userid:$password@/api/v1/libraries/$branchcode/holidays?from=2026-06-30&to=2026-06-01") + ->status_is(400) + ->json_is( '/error' => "'to' date must be after 'from' date" ); + + # 404 for non-existent library + my $non_existent_code = $library->branchcode; + $library->delete; + $cache->clear_from_cache( $non_existent_code . '_holidays' ); + + $t->get_ok("//$userid:$password@/api/v1/libraries/$non_existent_code/holidays?from=2026-06-01&to=2026-06-30") + ->status_is(404) + ->json_is( '/error' => 'Library not found' ); + + # Defaults work (no from/to params) + my $library2 = $builder->build_object( { class => 'Koha::Libraries' } ); + $t->get_ok( "//$userid:$password@/api/v1/libraries/" . $library2->branchcode . "/holidays" ) + ->status_is(200) + ->json_is( '' => [], 'Empty holidays for fresh library with defaults' ); + + $schema->storage->txn_rollback; +};