Bug 42206: Add GET /libraries/{id}/holidays endpoint

- Add list_holidays controller to Koha::REST::V1::Libraries
- Returns closed days for a library within a configurable date range
- Uses Koha::Calendar to determine holidays
- Defaults to today + 3 months if no range specified
- Add OpenAPI spec for the new endpoint
- Register route in swagger.yaml
- Add unit tests in t/db_dependent/api/v1/libraries.t

To test:
1. Apply patch
2. prove t/db_dependent/api/v1/libraries.t
3. Verify all tests pass, including the new list_holidays subtest
   which covers:
   - Holidays returned within a specified date range
   - Empty list when no holidays exist in range
   - 400 response when 'to' date precedes 'from' date
   - 404 response for non-existent library
   - Default range (no params) returns successfully

Sponsored-by: Büchereizentrale Schleswig-Holstein <https://www.bz-sh.de/>
Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
This commit is contained in:
Paul Derscheid 2026-03-27 09:38:45 +01:00 committed by Lucas Gass
parent cdf015b497
commit 2a3376b243
No known key found for this signature in database
GPG Key ID: 60B447398B0D36CA
4 changed files with 210 additions and 1 deletions

View File

@ -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;

View File

@ -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

View File

@ -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:

View File

@ -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;
};