From 8c682d3451a987480cf87b5ca8b9dfa1454c8416 Mon Sep 17 00:00:00 2001 From: Pedro Amorim Date: Tue, 15 Jul 2025 12:36:09 +0000 Subject: [PATCH] Bug 40395: Preparation: suspension_bulk REST API endpoint --- Koha/REST/V1/Holds.pm | 47 ++++++++++++++++++ api/v1/swagger/paths/holds.yaml | 61 +++++++++++++++++++++++ api/v1/swagger/swagger.yaml | 2 + t/db_dependent/api/v1/holds.t | 86 ++++++++++++++++++++++++++++++++- 4 files changed, 195 insertions(+), 1 deletion(-) diff --git a/Koha/REST/V1/Holds.pm b/Koha/REST/V1/Holds.pm index 33c885774c8..53abf9c2cdf 100644 --- a/Koha/REST/V1/Holds.pm +++ b/Koha/REST/V1/Holds.pm @@ -378,6 +378,53 @@ sub suspend { }; } +=head3 suspend_bulk + +Method that handles suspending multiple holds + +=cut + +sub suspend_bulk { + my $c = shift->openapi->valid_input or return; + + my $body = $c->req->json; + my $end_date = ($body) ? $body->{end_date} : undef; + my $hold_ids = ($body) ? $body->{hold_ids} : undef; + + return $c->render_resource_not_found("Hold") + unless $hold_ids; + + return try { + Koha::Database->new->schema->txn_do( + sub { + foreach my $hold_id (@$hold_ids) { + my $hold = Koha::Holds->find($hold_id); + + return $c->render_resource_not_found( "Hold", "id", $hold_id ) + unless $hold; + + $hold->suspend_hold($end_date); + $hold->discard_changes; + } + $c->res->headers->location( $c->req->url->to_string ); + return $c->render( + status => 201, + openapi => { + hold_ids => $hold_ids, + end_date => $end_date, + } + ); + } + ); + } catch { + if ( blessed $_ and $_->isa('Koha::Exceptions::Hold::CannotSuspendFound') ) { + return $c->render( status => 400, openapi => { error => "$_" } ); + } + + $c->unhandled_exception($_); + }; +} + =head3 resume Method that handles resuming a hold diff --git a/api/v1/swagger/paths/holds.yaml b/api/v1/swagger/paths/holds.yaml index 4fe775fb367..73e2b7982c8 100644 --- a/api/v1/swagger/paths/holds.yaml +++ b/api/v1/swagger/paths/holds.yaml @@ -279,6 +279,67 @@ x-koha-authorization: permissions: reserveforothers: "1" +"/holds/suspension_bulk": + post: + x-mojo-to: Holds#suspend_bulk + operationId: suspendHoldBulk + tags: + - holds + summary: Suspend the holds + parameters: + - name: body + in: body + description: Hold ids + required: true + schema: + type: object + properties: + end_date: + description: Date the hold suspension expires + type: string + format: date + hold_ids: + type: array + items: + type: integer + additionalProperties: false + consumes: + - application/json + produces: + - application/json + responses: + "201": + description: Holds suspended + "400": + description: Bad request + schema: + $ref: "../swagger.yaml#/definitions/error" + "401": + description: Authentication required + schema: + $ref: "../swagger.yaml#/definitions/error" + "403": + description: Hold not allowed + schema: + $ref: "../swagger.yaml#/definitions/error" + "404": + description: Hold 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: + reserveforothers: place_holds "/holds/{hold_id}": patch: x-mojo-to: Holds#edit diff --git a/api/v1/swagger/swagger.yaml b/api/v1/swagger/swagger.yaml index fb36579cea9..71f1e00f2b6 100644 --- a/api/v1/swagger/swagger.yaml +++ b/api/v1/swagger/swagger.yaml @@ -399,6 +399,8 @@ paths: $ref: ./paths/extended_attribute_types.yaml#/~1extended_attribute_types /holds: $ref: ./paths/holds.yaml#/~1holds + "/holds/suspension_bulk": + $ref: "./paths/holds.yaml#/~1holds~1suspension_bulk" "/holds/{hold_id}": $ref: "./paths/holds.yaml#/~1holds~1{hold_id}" "/holds/{hold_id}/pickup_location": diff --git a/t/db_dependent/api/v1/holds.t b/t/db_dependent/api/v1/holds.t index 9f13cb3a4dd..f27f171db86 100755 --- a/t/db_dependent/api/v1/holds.t +++ b/t/db_dependent/api/v1/holds.t @@ -17,7 +17,7 @@ use Modern::Perl; -use Test::More tests => 16; +use Test::More tests => 17; use Test::MockModule; use Test::Mojo; use t::lib::TestBuilder; @@ -518,6 +518,90 @@ subtest 'suspend and resume tests' => sub { $schema->storage->txn_rollback; }; +subtest 'suspend bulk tests' => sub { + + plan tests => 16; + + $schema->storage->txn_begin; + + my $password = 'AbcdEFG123'; + + my $patron = $builder->build_object( { class => 'Koha::Patrons', value => { userid => 'tomasito', flags => 0 } } ); + $builder->build( + { + source => 'UserPermission', + value => { + borrowernumber => $patron->borrowernumber, + module_bit => 6, + code => 'place_holds', + }, + } + ); + + $patron->set_password( { password => $password, skip_validation => 1 } ); + my $userid = $patron->userid; + + # Disable logging + t::lib::Mocks::mock_preference( 'HoldsLog', 0 ); + t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 ); + + my $hold = $builder->build_object( + { + class => 'Koha::Holds', + value => { suspend => 0, suspend_until => undef, waitingdate => undef, found => undef } + } + ); + + my $hold_2 = $builder->build_object( + { + class => 'Koha::Holds', + value => { suspend => 0, suspend_until => undef, waitingdate => undef, found => undef } + } + ); + + ok( !$hold->is_suspended, 'Hold is not suspended' ); + ok( !$hold_2->is_suspended, 'Hold is not suspended' ); + + $t->post_ok( + "//$userid:$password@/api/v1/holds/suspension_bulk" => json => { hold_ids => [ $hold->id, $hold_2->id ] } ) + ->status_is( 201, 'Hold bulk suspension created' ); + + $hold->discard_changes; + $hold_2->discard_changes; + + ok( $hold->is_suspended, 'Hold is suspended' ); + ok( $hold_2->is_suspended, 'Hold is suspended' ); + + $hold->resume; + $hold_2->resume; + + ok( !$hold->is_suspended, 'Hold is not suspended' ); + ok( !$hold_2->is_suspended, 'Hold is not suspended' ); + + $t->post_ok( "//$userid:$password@/api/v1/holds/suspension_bulk" => json => + { end_date => "2024-07-30", hold_ids => [ $hold->id, $hold_2->id ] } ) + ->status_is( 201, 'Hold bulk suspension created' ) + ->json_is( { end_date => "2024-07-30", hold_ids => [ $hold->id, $hold_2->id ] } ); + + $hold->discard_changes; + $hold_2->discard_changes; + + $hold_2->delete; + + $hold->resume; + ok( !$hold->is_suspended, 'Hold is not suspended' ); + + $t->post_ok( "//$userid:$password@/api/v1/holds/suspension_bulk" => json => { hold_ids => [ $hold_2->id ] } ) + ->status_is( 404, 'Hold bulk suspension failed. Hold not found' ); + ok( !$hold->is_suspended, 'Hold is not suspended. Bulk suspension failed' ); + + $hold->discard_changes; + + ok( !$hold->is_suspended, 'Hold is not suspended. Bulk suspension failed' ); + + $schema->storage->txn_rollback; +}; + subtest 'PUT /holds/{hold_id}/priority tests' => sub { plan tests => 14; -- 2.39.5