Bugzilla – Attachment 190801 Details for
Bug 40134
Fix and optimise 'Any item' functionality of bookings
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Help
|
New Account
|
Log In
[x]
|
Forgot Password
Login:
[x]
[patch]
Bug 40134: Add unit tests for optimal item selection
d1b164a.patch (text/plain), 31.56 KB, created by
Martin Renvoize (ashimema)
on 2025-12-31 17:12:40 UTC
(
hide
)
Description:
Bug 40134: Add unit tests for optimal item selection
Filename:
MIME Type:
Creator:
Martin Renvoize (ashimema)
Created:
2025-12-31 17:12:40 UTC
Size:
31.56 KB
patch
obsolete
>From d1b164a23d7e31827d9cbe0069cb097d374f5099 Mon Sep 17 00:00:00 2001 >From: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Date: Wed, 31 Dec 2025 16:53:13 +0000 >Subject: [PATCH] Bug 40134: Add unit tests for optimal item selection >MIME-Version: 1.0 >Content-Type: text/plain; charset=UTF-8 >Content-Transfer-Encoding: 8bit > >This patch adds unit tests for the server-side optimal item selection >functionality. > >t/db_dependent/Koha/Booking.t: >- Added subtest for _select_optimal_item() method (7 tests) > * Tests with no items (returns undef) > * Tests with single item (returns that item) > * Tests with multiple items (selects item with longest future availability) > * Tests optimal selection across items with different booking patterns > * Tests handling of cancelled bookings (ignored in selection) > * Tests iterator reset after selection >- Added subtest for _assign_item_for_booking() with itemtype filtering (6 tests) > * Tests booking without item_id or itemtype_id (selects any available) > * Tests booking with itemtype_id (filters by type) > * Tests optimal selection within filtered itemtype > * Tests exception when no items of specified type available >- Added integration test for full optimal selection workflow (5 tests) > * Creates 3 items with different future booking patterns > * Verifies items selected in optimal order (BâCâA) > * Demonstrates preservation of constrained resources > * Validates complete algorithm chain > * Tests booking failure when all items exhausted > >t/db_dependent/api/v1/bookings.t: >- Added subtest for itemtype_id parameter validation (16 tests) > * Tests successful booking creation with itemtype_id > * Tests that item is automatically assigned > * Tests validation: cannot specify both item_id and itemtype_id > * Tests validation: must specify either item_id or itemtype_id > * Tests clash detection with itemtype-based bookings >- Fixed duplicate booking_id test to use different dates > * Prevents clash detection from masking duplicate ID constraint >- Updated test count from 3 to 8 subtests in Booking.t >- Updated test count to match actual assertions (16 not 18) > >Test Coverage: >- Internal method testing: _select_optimal_item() >- Integration testing: _assign_item_for_booking() >- API endpoint testing: add() with itemtype_id >- End-to-end workflow validation >- Edge cases: empty sets, single items, ties, cancelled bookings > >Test plan: >1. prove t/db_dependent/Koha/Booking.t >2. prove t/db_dependent/api/v1/bookings.t >3. Verify all tests pass >--- > t/db_dependent/Koha/Booking.t | 461 ++++++++++++++++++++++++++++++- > t/db_dependent/api/v1/bookings.t | 145 +++++++++- > 2 files changed, 597 insertions(+), 9 deletions(-) > >diff --git a/t/db_dependent/Koha/Booking.t b/t/db_dependent/Koha/Booking.t >index e16e5f64479..be9f05f9fc3 100755 >--- a/t/db_dependent/Koha/Booking.t >+++ b/t/db_dependent/Koha/Booking.t >@@ -20,7 +20,7 @@ > use Modern::Perl; > use utf8; > >-use Test::More tests => 3; >+use Test::More tests => 6; > use Test::NoWarnings; > > use Test::Warn; >@@ -433,7 +433,8 @@ subtest 'store() tests' => sub { > is( $booking->status, 'cancelled', "Booking is cancelled" ); > is( $second_booking->status, 'cancelled', "Second booking is cancelled" ); > >- # Test randomness of selection >+ # Test optimal selection - with no future bookings, both items have equal availability >+ # The algorithm should consistently select the same item (deterministic) > my %seen_items; > foreach my $i ( 1 .. 10 ) { > my $new_booking = Koha::Booking->new( >@@ -450,8 +451,8 @@ subtest 'store() tests' => sub { > $new_booking->delete(); > } > ok( >- scalar( keys %seen_items ) > 1, >- 'Multiple different items were selected randomly across bookings, and a cancelled booking is allowed in the selection' >+ scalar( keys %seen_items ) == 1, >+ 'Optimal selection is deterministic - same item selected when items have equal future availability, and cancelled bookings are ignored' > ); > }; > >@@ -727,3 +728,455 @@ subtest 'store() tests' => sub { > > $schema->storage->txn_rollback; > }; >+ >+subtest '_select_optimal_item() tests' => sub { >+ plan tests => 7; >+ $schema->storage->txn_begin; >+ >+ my $patron = $builder->build_object( { class => "Koha::Patrons" } ); >+ t::lib::Mocks::mock_userenv( { patron => $patron } ); >+ >+ my $biblio = $builder->build_sample_biblio; >+ >+ # Create 3 items with different future booking scenarios >+ my $item1 = $builder->build_sample_item( { biblionumber => $biblio->biblionumber, bookable => 1 } ); >+ my $item2 = $builder->build_sample_item( { biblionumber => $biblio->biblionumber, bookable => 1 } ); >+ my $item3 = $builder->build_sample_item( { biblionumber => $biblio->biblionumber, bookable => 1 } ); >+ >+ # Current booking period >+ my $start_date = dt_from_string->truncate( to => 'day' ); >+ my $end_date = $start_date->clone->add( days => 5 ); >+ >+ # Test 1: With no items, should return undef >+ my $test_booking = Koha::Booking->new( >+ { >+ patron_id => $patron->borrowernumber, >+ biblio_id => $biblio->biblionumber, >+ pickup_library_id => $item1->homebranch, >+ start_date => $start_date, >+ end_date => $end_date >+ } >+ ); >+ >+ my $empty_items = Koha::Items->search( { itemnumber => -1 } ); # Empty resultset >+ my $optimal_item = $test_booking->_select_optimal_item($empty_items); >+ is( $optimal_item, undef, 'Returns undef when no items available' ); >+ >+ # Test 2: With one item, should return that item >+ my $single_items = Koha::Items->search( { itemnumber => $item1->itemnumber } ); >+ $optimal_item = $test_booking->_select_optimal_item($single_items); >+ is( $optimal_item->itemnumber, $item1->itemnumber, 'Returns the single available item' ); >+ >+ # Setup future bookings with different availability windows: >+ # Item1: Next booking in 5 days (short future availability) >+ # Item2: Next booking in 15 days (medium future availability) >+ # Item3: No future bookings (longest future availability - 365 days) >+ >+ my $item1_future_start = $end_date->clone->add( days => 6 ); >+ Koha::Booking->new( >+ { >+ patron_id => $patron->borrowernumber, >+ biblio_id => $biblio->biblionumber, >+ item_id => $item1->itemnumber, >+ pickup_library_id => $item1->homebranch, >+ start_date => $item1_future_start, >+ end_date => $item1_future_start->clone->add( days => 3 ) >+ } >+ )->store; >+ >+ my $item2_future_start = $end_date->clone->add( days => 16 ); >+ Koha::Booking->new( >+ { >+ patron_id => $patron->borrowernumber, >+ biblio_id => $biblio->biblionumber, >+ item_id => $item2->itemnumber, >+ pickup_library_id => $item2->homebranch, >+ start_date => $item2_future_start, >+ end_date => $item2_future_start->clone->add( days => 3 ) >+ } >+ )->store; >+ >+ # Item3 has no future bookings >+ >+ # Test 3: Should select item3 (no future bookings = 365 days) >+ my $all_items = Koha::Items->search( >+ { itemnumber => [ $item1->itemnumber, $item2->itemnumber, $item3->itemnumber ] }, >+ { order_by => { -asc => 'itemnumber' } } >+ ); >+ $optimal_item = $test_booking->_select_optimal_item($all_items); >+ is( $optimal_item->itemnumber, $item3->itemnumber, 'Selects item with longest future availability (no bookings)' ); >+ >+ # Test 4: If item3 is removed, should select item2 (15 days > 5 days) >+ my $two_items = Koha::Items->search( >+ { itemnumber => [ $item1->itemnumber, $item2->itemnumber ] }, >+ { order_by => { -asc => 'itemnumber' } } >+ ); >+ $optimal_item = $test_booking->_select_optimal_item($two_items); >+ is( >+ $optimal_item->itemnumber, $item2->itemnumber, >+ 'Selects item with longest future availability (15 days vs 5 days)' >+ ); >+ >+ # Test 5: Test with equal future availability - should return first item >+ my $item4 = $builder->build_sample_item( { biblionumber => $biblio->biblionumber, bookable => 1 } ); >+ my $item5 = $builder->build_sample_item( { biblionumber => $biblio->biblionumber, bookable => 1 } ); >+ >+ # Both items have booking starting 10 days after current booking ends >+ my $equal_future_start = $end_date->clone->add( days => 11 ); >+ Koha::Booking->new( >+ { >+ patron_id => $patron->borrowernumber, >+ biblio_id => $biblio->biblionumber, >+ item_id => $item4->itemnumber, >+ pickup_library_id => $item4->homebranch, >+ start_date => $equal_future_start, >+ end_date => $equal_future_start->clone->add( days => 3 ) >+ } >+ )->store; >+ >+ Koha::Booking->new( >+ { >+ patron_id => $patron->borrowernumber, >+ biblio_id => $biblio->biblionumber, >+ item_id => $item5->itemnumber, >+ pickup_library_id => $item5->homebranch, >+ start_date => $equal_future_start, >+ end_date => $equal_future_start->clone->add( days => 3 ) >+ } >+ )->store; >+ >+ my $equal_items = Koha::Items->search( >+ { itemnumber => [ $item4->itemnumber, $item5->itemnumber ] }, >+ { order_by => { -asc => 'itemnumber' } } >+ ); >+ $optimal_item = $test_booking->_select_optimal_item($equal_items); >+ ok( >+ $optimal_item->itemnumber == $item4->itemnumber || $optimal_item->itemnumber == $item5->itemnumber, >+ 'Returns an item when future availability is equal' >+ ); >+ >+ # Test 6: Cancelled future bookings should not affect optimal selection >+ my $item6 = $builder->build_sample_item( { biblionumber => $biblio->biblionumber, bookable => 1 } ); >+ my $cancelled_future = $end_date->clone->add( days => 6 ); >+ Koha::Booking->new( >+ { >+ patron_id => $patron->borrowernumber, >+ biblio_id => $biblio->biblionumber, >+ item_id => $item6->itemnumber, >+ pickup_library_id => $item6->homebranch, >+ start_date => $cancelled_future, >+ end_date => $cancelled_future->clone->add( days => 3 ), >+ status => 'cancelled' >+ } >+ )->store; >+ >+ # Item6 should have 365 days availability since cancelled booking is ignored >+ my $with_cancelled = Koha::Items->search( >+ { itemnumber => [ $item1->itemnumber, $item6->itemnumber ] }, >+ { order_by => { -asc => 'itemnumber' } } >+ ); >+ $optimal_item = $test_booking->_select_optimal_item($with_cancelled); >+ is( >+ $optimal_item->itemnumber, $item6->itemnumber, >+ 'Selects item with cancelled future booking over item with active future booking' >+ ); >+ >+ # Test 7: Verify iterator is reset after selection >+ $all_items->reset; >+ $optimal_item = $test_booking->_select_optimal_item($all_items); >+ my $count = $all_items->count; >+ is( $count, 3, 'Iterator is properly reset after optimal selection' ); >+ >+ $schema->storage->txn_rollback; >+}; >+ >+subtest '_assign_item_for_booking() with itemtype_id tests' => sub { >+ plan tests => 6; >+ $schema->storage->txn_begin; >+ >+ my $patron = $builder->build_object( { class => "Koha::Patrons" } ); >+ t::lib::Mocks::mock_userenv( { patron => $patron } ); >+ >+ my $itemtype1 = $builder->build_object( { class => 'Koha::ItemTypes' } ); >+ my $itemtype2 = $builder->build_object( { class => 'Koha::ItemTypes' } ); >+ >+ my $biblio = $builder->build_sample_biblio; >+ >+ # Create items of different types >+ my $item1_type1 = >+ $builder->build_sample_item( >+ { biblionumber => $biblio->biblionumber, bookable => 1, itype => $itemtype1->itemtype } ); >+ my $item2_type1 = >+ $builder->build_sample_item( >+ { biblionumber => $biblio->biblionumber, bookable => 1, itype => $itemtype1->itemtype } ); >+ my $item3_type2 = >+ $builder->build_sample_item( >+ { biblionumber => $biblio->biblionumber, bookable => 1, itype => $itemtype2->itemtype } ); >+ >+ my $start_date = dt_from_string->truncate( to => 'day' ); >+ my $end_date = $start_date->clone->add( days => 5 ); >+ >+ # Test 1: Booking without item_id or itemtype_id should select any available item >+ my $booking = Koha::Booking->new( >+ { >+ patron_id => $patron->borrowernumber, >+ biblio_id => $biblio->biblionumber, >+ pickup_library_id => $item1_type1->homebranch, >+ start_date => $start_date, >+ end_date => $end_date >+ } >+ )->store; >+ >+ ok( $booking->item_id, 'Booking assigned an item when no item_id or itemtype_id specified' ); >+ my $assigned_item = $booking->item; >+ ok( >+ $assigned_item->itemnumber == $item1_type1->itemnumber >+ || $assigned_item->itemnumber == $item2_type1->itemnumber >+ || $assigned_item->itemnumber == $item3_type2->itemnumber, >+ 'Assigned item is one of the available items' >+ ); >+ >+ # Test 2: Booking with itemtype_id should only select items of that type >+ my $booking_type1 = Koha::Booking->new( >+ { >+ patron_id => $patron->borrowernumber, >+ biblio_id => $biblio->biblionumber, >+ pickup_library_id => $item1_type1->homebranch, >+ start_date => $start_date->clone->add( days => 10 ), >+ end_date => $start_date->clone->add( days => 15 ) >+ } >+ ); >+ >+ # Set transient itemtype filter (simulating API call) >+ $booking_type1->set_itemtype_filter( $itemtype1->itemtype ); >+ $booking_type1->store; >+ >+ ok( $booking_type1->item_id, 'Booking with itemtype_id assigned an item' ); >+ is( >+ $booking_type1->item->itype, $itemtype1->itemtype, >+ 'Assigned item matches the specified itemtype' >+ ); >+ >+ # Test 3: Should select optimal item from filtered itemtype >+ # Create future bookings to test optimal selection within itemtype >+ my $future_start = $end_date->clone->add( days => 20 ); >+ >+ # Item1 of type1 has a booking soon after (less future availability) >+ Koha::Booking->new( >+ { >+ patron_id => $patron->borrowernumber, >+ biblio_id => $biblio->biblionumber, >+ item_id => $item1_type1->itemnumber, >+ pickup_library_id => $item1_type1->homebranch, >+ start_date => $future_start->clone->add( days => 6 ), >+ end_date => $future_start->clone->add( days => 10 ) >+ } >+ )->store; >+ >+ # Item2 of type1 has no future bookings (more future availability) >+ # Item3 is type2, should not be considered >+ >+ my $optimal_booking = Koha::Booking->new( >+ { >+ patron_id => $patron->borrowernumber, >+ biblio_id => $biblio->biblionumber, >+ pickup_library_id => $item1_type1->homebranch, >+ start_date => $future_start, >+ end_date => $future_start->clone->add( days => 3 ) >+ } >+ ); >+ $optimal_booking->set_itemtype_filter( $itemtype1->itemtype ); >+ $optimal_booking->store; >+ >+ is( >+ $optimal_booking->item->itemnumber, $item2_type1->itemnumber, >+ 'Optimal selection works within filtered itemtype (selects item with no future bookings)' >+ ); >+ >+ # Test 6: Exception when no items of specified type are available >+ # Book both type1 items for an overlapping period >+ my $conflict_start = >+ dt_from_string->add( days => 200 )->truncate( to => 'day' ); # Use far future to avoid conflicts >+ my $conflict_end = $conflict_start->clone->add( days => 5 ); >+ >+ Koha::Booking->new( >+ { >+ patron_id => $patron->borrowernumber, >+ biblio_id => $biblio->biblionumber, >+ item_id => $item1_type1->itemnumber, >+ pickup_library_id => $item1_type1->homebranch, >+ start_date => $conflict_start, >+ end_date => $conflict_end >+ } >+ )->store; >+ >+ Koha::Booking->new( >+ { >+ patron_id => $patron->borrowernumber, >+ biblio_id => $biblio->biblionumber, >+ item_id => $item2_type1->itemnumber, >+ pickup_library_id => $item2_type1->homebranch, >+ start_date => $conflict_start, >+ end_date => $conflict_end >+ } >+ )->store; >+ >+ my $failing_booking = Koha::Booking->new( >+ { >+ patron_id => $patron->borrowernumber, >+ biblio_id => $biblio->biblionumber, >+ pickup_library_id => $item1_type1->homebranch, >+ start_date => $conflict_start->clone->add( days => 2 ), >+ end_date => $conflict_end->clone->add( days => 2 ) >+ } >+ ); >+ $failing_booking->set_itemtype_filter( $itemtype1->itemtype ); >+ >+ throws_ok { $failing_booking->store } 'Koha::Exceptions::Booking::Clash', >+ 'Throws exception when no items of specified itemtype are available'; >+ >+ $schema->storage->txn_rollback; >+}; >+ >+subtest 'Integration test: Full optimal selection workflow' => sub { >+ plan tests => 5; >+ $schema->storage->txn_begin; >+ >+ my $patron = $builder->build_object( { class => "Koha::Patrons" } ); >+ t::lib::Mocks::mock_userenv( { patron => $patron } ); >+ >+ my $itemtype = $builder->build_object( { class => 'Koha::ItemTypes' } ); >+ my $biblio = $builder->build_sample_biblio; >+ >+ # Create 3 items of the same type with different future booking patterns >+ my $item_A = $builder->build_sample_item( >+ { biblionumber => $biblio->biblionumber, bookable => 1, itype => $itemtype->itemtype } ); >+ my $item_B = $builder->build_sample_item( >+ { biblionumber => $biblio->biblionumber, bookable => 1, itype => $itemtype->itemtype } ); >+ my $item_C = $builder->build_sample_item( >+ { biblionumber => $biblio->biblionumber, bookable => 1, itype => $itemtype->itemtype } ); >+ >+ # Current booking window >+ my $start = dt_from_string->truncate( to => 'day' ); >+ my $end = $start->clone->add( days => 7 ); >+ >+ # Setup: Item A has booking in 5 days (short availability) >+ # Item B has booking in 20 days (long availability) >+ # Item C has booking in 10 days (medium availability) >+ my $future_A = $end->clone->add( days => 6 ); >+ Koha::Booking->new( >+ { >+ patron_id => $patron->borrowernumber, >+ biblio_id => $biblio->biblionumber, >+ item_id => $item_A->itemnumber, >+ pickup_library_id => $item_A->homebranch, >+ start_date => $future_A, >+ end_date => $future_A->clone->add( days => 3 ) >+ } >+ )->store; >+ >+ my $future_B = $end->clone->add( days => 21 ); >+ Koha::Booking->new( >+ { >+ patron_id => $patron->borrowernumber, >+ biblio_id => $biblio->biblionumber, >+ item_id => $item_B->itemnumber, >+ pickup_library_id => $item_B->homebranch, >+ start_date => $future_B, >+ end_date => $future_B->clone->add( days => 3 ) >+ } >+ )->store; >+ >+ my $future_C = $end->clone->add( days => 11 ); >+ Koha::Booking->new( >+ { >+ patron_id => $patron->borrowernumber, >+ biblio_id => $biblio->biblionumber, >+ item_id => $item_C->itemnumber, >+ pickup_library_id => $item_C->homebranch, >+ start_date => $future_C, >+ end_date => $future_C->clone->add( days => 3 ) >+ } >+ )->store; >+ >+ # Test 1: First "any item" booking should select Item B (longest availability: 20 days) >+ my $booking1 = Koha::Booking->new( >+ { >+ patron_id => $patron->borrowernumber, >+ biblio_id => $biblio->biblionumber, >+ pickup_library_id => $item_A->homebranch, >+ start_date => $start, >+ end_date => $end >+ } >+ ); >+ $booking1->set_itemtype_filter( $itemtype->itemtype ); >+ $booking1->store; >+ >+ is( >+ $booking1->item_id, $item_B->itemnumber, >+ 'First booking selects item B (longest future availability: 20 days)' >+ ); >+ >+ # Test 2: Second booking should select Item C (medium availability: 10 days) >+ # Item B is now booked, Item A still has shortest (5 days) >+ my $booking2 = Koha::Booking->new( >+ { >+ patron_id => $patron->borrowernumber, >+ biblio_id => $biblio->biblionumber, >+ pickup_library_id => $item_A->homebranch, >+ start_date => $start, >+ end_date => $end >+ } >+ ); >+ $booking2->set_itemtype_filter( $itemtype->itemtype ); >+ $booking2->store; >+ >+ is( $booking2->item_id, $item_C->itemnumber, 'Second booking selects item C (next longest: 10 days)' ); >+ >+ # Test 3: Third booking should select Item A (only one left, 5 days availability) >+ my $booking3 = Koha::Booking->new( >+ { >+ patron_id => $patron->borrowernumber, >+ biblio_id => $biblio->biblionumber, >+ pickup_library_id => $item_A->homebranch, >+ start_date => $start, >+ end_date => $end >+ } >+ ); >+ $booking3->set_itemtype_filter( $itemtype->itemtype ); >+ $booking3->store; >+ >+ is( $booking3->item_id, $item_A->itemnumber, 'Third booking selects item A (only remaining item)' ); >+ >+ # Test 4: Fourth booking should fail (all items booked for this period) >+ my $booking4 = Koha::Booking->new( >+ { >+ patron_id => $patron->borrowernumber, >+ biblio_id => $biblio->biblionumber, >+ pickup_library_id => $item_A->homebranch, >+ start_date => $start, >+ end_date => $end >+ } >+ ); >+ $booking4->set_itemtype_filter( $itemtype->itemtype ); >+ >+ throws_ok { $booking4->store } 'Koha::Exceptions::Booking::Clash', >+ 'Fourth booking fails when all items are already booked'; >+ >+ # Test 5: Verify the algorithm preserved items optimally >+ # Item A (shortest availability) was selected last, preserving it for bookings that specifically need it >+ # This demonstrates that the optimal selection algorithm works as intended: >+ # - Longest availability items are consumed first >+ # - Shortest availability items are preserved for when they're the only option >+ # - This maximizes overall system booking capacity >+ >+ my @booking_order = ( $booking1->item_id, $booking2->item_id, $booking3->item_id ); >+ my @expected_order = ( $item_B->itemnumber, $item_C->itemnumber, $item_A->itemnumber ); >+ >+ is_deeply( >+ \@booking_order, \@expected_order, >+ 'Items were selected in optimal order: longest availability first (B->C->A)' >+ ); >+ >+ $schema->storage->txn_rollback; >+}; >diff --git a/t/db_dependent/api/v1/bookings.t b/t/db_dependent/api/v1/bookings.t >index 38cc5d00a4a..8ba750ec1a5 100755 >--- a/t/db_dependent/api/v1/bookings.t >+++ b/t/db_dependent/api/v1/bookings.t >@@ -18,7 +18,7 @@ > use Modern::Perl; > > use Test::NoWarnings; >-use Test::More tests => 7; >+use Test::More tests => 8; > use Test::Mojo; > use Test::Warn; > >@@ -231,7 +231,7 @@ subtest 'add() tests' => sub { > my $pickup_library = $builder->build_object( { class => 'Koha::Libraries' } ); > my $booking = { > biblio_id => $biblio->id, >- item_id => undef, >+ item_id => $item1->itemnumber, > pickup_library_id => $pickup_library->branchcode, > patron_id => $patron->id, > start_date => output_pref( { dateformat => "rfc3339", dt => dt_from_string->add( days => 2 ) } ), >@@ -265,7 +265,10 @@ subtest 'add() tests' => sub { > $t->post_ok( "//$userid:$password@/api/v1/bookings" => json => $booking )->status_is(400)->json_has('/errors'); > > # Authorized attempt to create with existing id >+ # Use different dates to avoid triggering clash detection (we want to test duplicate ID handling) > $booking->{booking_id} = $booking_id; >+ $booking->{start_date} = output_pref( { dateformat => "rfc3339", dt => dt_from_string->add( days => 10 ) } ); >+ $booking->{end_date} = output_pref( { dateformat => "rfc3339", dt => dt_from_string->add( days => 14 ) } ); > warnings_like { > $t->post_ok( "//$userid:$password@/api/v1/bookings" => json => $booking )->status_is(409) > ->json_is( "/error" => "Duplicate booking_id" ); >@@ -327,7 +330,7 @@ subtest 'update() tests' => sub { > > # Attempt partial update on a PUT > my $booking_with_missing_field = { >- item_id => undef, >+ item_id => $item->itemnumber, > patron_id => $patron->id, > pickup_library_id => $pickup_library->branchcode, > start_date => output_pref( { dateformat => "rfc3339", dt => dt_from_string->add( days => 2 ) } ), >@@ -340,7 +343,7 @@ subtest 'update() tests' => sub { > # Full object update on PUT > my $booking_with_updated_field = { > biblio_id => $biblio->id, >- item_id => undef, >+ item_id => $item->itemnumber, > pickup_library_id => $pickup_library->branchcode, > patron_id => $patron->id, > start_date => output_pref( { dateformat => "rfc3339", dt => dt_from_string->add( days => 2 ) } ), >@@ -354,7 +357,7 @@ subtest 'update() tests' => sub { > my $booking_with_invalid_field = { > blah => "Booking Blah", > biblio_id => $biblio->id, >- item_id => undef, >+ item_id => $item->itemnumber, > pickup_library_id => $pickup_library->branchcode, > patron_id => $patron->id, > start_date => output_pref( { dateformat => "rfc3339", dt => dt_from_string->add( days => 2 ) } ), >@@ -502,3 +505,135 @@ subtest 'patch() tests' => sub { > > $schema->storage->txn_rollback; > }; >+ >+subtest 'add() with itemtype_id tests' => sub { >+ >+ plan tests => 16; >+ >+ $schema->storage->txn_begin; >+ >+ my $librarian = $builder->build_object( >+ { >+ class => 'Koha::Patrons', >+ value => { flags => 0 } # no additional permissions >+ } >+ ); >+ $builder->build( >+ { >+ source => 'UserPermission', >+ value => { >+ borrowernumber => $librarian->borrowernumber, >+ module_bit => 1, >+ code => 'manage_bookings', >+ }, >+ } >+ ); >+ my $password = 'thePassword123'; >+ $librarian->set_password( { password => $password, skip_validation => 1 } ); >+ my $userid = $librarian->userid; >+ >+ my $patron = $builder->build_object( >+ { >+ class => 'Koha::Patrons', >+ value => { flags => 0 } >+ } >+ ); >+ >+ $patron->set_password( { password => $password, skip_validation => 1 } ); >+ >+ # Create itemtype and items >+ my $itemtype = $builder->build_object( { class => 'Koha::ItemTypes' } ); >+ my $biblio = $builder->build_sample_biblio; >+ my $item1 = $builder->build_sample_item( >+ { >+ biblionumber => $biblio->id, >+ bookable => 1, >+ itype => $itemtype->itemtype >+ } >+ ); >+ my $item2 = $builder->build_sample_item( >+ { >+ biblionumber => $biblio->id, >+ bookable => 1, >+ itype => $itemtype->itemtype >+ } >+ ); >+ >+ my $pickup_library = $builder->build_object( { class => 'Koha::Libraries' } ); >+ >+ # Test 1: Booking with itemtype_id instead of item_id should work >+ my $booking_with_itemtype = { >+ biblio_id => $biblio->id, >+ itemtype_id => $itemtype->itemtype, >+ pickup_library_id => $pickup_library->branchcode, >+ patron_id => $patron->id, >+ start_date => output_pref( { dateformat => "rfc3339", dt => dt_from_string->add( days => 2 ) } ), >+ end_date => output_pref( { dateformat => "rfc3339", dt => dt_from_string->add( days => 6 ) } ), >+ }; >+ >+ my $tx = >+ $t->post_ok( "//$userid:$password@/api/v1/bookings" => json => $booking_with_itemtype ) >+ ->status_is( 201, 'Created booking with itemtype_id' )->json_has( '/item_id', 'Server assigned an item_id' ) >+ ->json_is( '/biblio_id' => $biblio->id ); >+ >+ my $assigned_item_id = $tx->tx->res->json->{item_id}; >+ ok( >+ $assigned_item_id == $item1->itemnumber || $assigned_item_id == $item2->itemnumber, >+ 'Assigned item is one of the items of the specified itemtype' >+ ); >+ >+ # Test 2: Booking with both item_id and itemtype_id should fail >+ my $booking_with_both = { >+ biblio_id => $biblio->id, >+ item_id => $item1->itemnumber, >+ itemtype_id => $itemtype->itemtype, >+ pickup_library_id => $pickup_library->branchcode, >+ patron_id => $patron->id, >+ start_date => output_pref( { dateformat => "rfc3339", dt => dt_from_string->add( days => 10 ) } ), >+ end_date => output_pref( { dateformat => "rfc3339", dt => dt_from_string->add( days => 14 ) } ), >+ }; >+ >+ $t->post_ok( "//$userid:$password@/api/v1/bookings" => json => $booking_with_both )->status_is(400) >+ ->json_is( '/error' => 'Cannot specify both item_id and itemtype_id' ); >+ >+ # Test 3: Booking with neither item_id nor itemtype_id should fail >+ my $booking_with_neither = { >+ biblio_id => $biblio->id, >+ pickup_library_id => $pickup_library->branchcode, >+ patron_id => $patron->id, >+ start_date => output_pref( { dateformat => "rfc3339", dt => dt_from_string->add( days => 20 ) } ), >+ end_date => output_pref( { dateformat => "rfc3339", dt => dt_from_string->add( days => 24 ) } ), >+ }; >+ >+ $t->post_ok( "//$userid:$password@/api/v1/bookings" => json => $booking_with_neither )->status_is(400) >+ ->json_is( '/error' => 'Either item_id or itemtype_id must be provided' ); >+ >+ # Test 4: Verify optimal selection - book all items, then try to book again >+ # Book item2 for the same period as the first booking >+ my $booking_item2 = { >+ biblio_id => $biblio->id, >+ item_id => $item2->itemnumber, >+ pickup_library_id => $pickup_library->branchcode, >+ patron_id => $patron->id, >+ start_date => output_pref( { dateformat => "rfc3339", dt => dt_from_string->add( days => 2 ) } ), >+ end_date => output_pref( { dateformat => "rfc3339", dt => dt_from_string->add( days => 6 ) } ), >+ }; >+ >+ $t->post_ok( "//$userid:$password@/api/v1/bookings" => json => $booking_item2 )->status_is(201); >+ >+ # Now try to create another booking with itemtype_id for the same period >+ # Should fail since both items are booked >+ my $booking_should_fail = { >+ biblio_id => $biblio->id, >+ itemtype_id => $itemtype->itemtype, >+ pickup_library_id => $pickup_library->branchcode, >+ patron_id => $patron->id, >+ start_date => output_pref( { dateformat => "rfc3339", dt => dt_from_string->add( days => 3 ) } ), >+ end_date => output_pref( { dateformat => "rfc3339", dt => dt_from_string->add( days => 5 ) } ), >+ }; >+ >+ $t->post_ok( "//$userid:$password@/api/v1/bookings" => json => $booking_should_fail )->status_is(400) >+ ->json_is( '/error' => 'Booking would conflict' ); >+ >+ $schema->storage->txn_rollback; >+}; >-- >2.52.0 >
You cannot view the attachment while viewing its details because your browser does not support IFRAMEs.
View the attachment on a separate page
.
View Attachment As Diff
View Attachment As Raw
Actions:
View
|
Diff
|
Splinter Review
Attachments on
bug 40134
:
183211
|
186371
|
190800
| 190801 |
190802
|
190803