|
Lines 20-26
Link Here
|
| 20 |
use Modern::Perl; |
20 |
use Modern::Perl; |
| 21 |
use utf8; |
21 |
use utf8; |
| 22 |
|
22 |
|
| 23 |
use Test::More tests => 3; |
23 |
use Test::More tests => 6; |
| 24 |
use Test::NoWarnings; |
24 |
use Test::NoWarnings; |
| 25 |
|
25 |
|
| 26 |
use Test::Warn; |
26 |
use Test::Warn; |
|
Lines 433-439
subtest 'store() tests' => sub {
Link Here
|
| 433 |
is( $booking->status, 'cancelled', "Booking is cancelled" ); |
433 |
is( $booking->status, 'cancelled', "Booking is cancelled" ); |
| 434 |
is( $second_booking->status, 'cancelled', "Second booking is cancelled" ); |
434 |
is( $second_booking->status, 'cancelled', "Second booking is cancelled" ); |
| 435 |
|
435 |
|
| 436 |
# Test randomness of selection |
436 |
# Test optimal selection - with no future bookings, both items have equal availability |
|
|
437 |
# The algorithm should consistently select the same item (deterministic) |
| 437 |
my %seen_items; |
438 |
my %seen_items; |
| 438 |
foreach my $i ( 1 .. 10 ) { |
439 |
foreach my $i ( 1 .. 10 ) { |
| 439 |
my $new_booking = Koha::Booking->new( |
440 |
my $new_booking = Koha::Booking->new( |
|
Lines 450-457
subtest 'store() tests' => sub {
Link Here
|
| 450 |
$new_booking->delete(); |
451 |
$new_booking->delete(); |
| 451 |
} |
452 |
} |
| 452 |
ok( |
453 |
ok( |
| 453 |
scalar( keys %seen_items ) > 1, |
454 |
scalar( keys %seen_items ) == 1, |
| 454 |
'Multiple different items were selected randomly across bookings, and a cancelled booking is allowed in the selection' |
455 |
'Optimal selection is deterministic - same item selected when items have equal future availability, and cancelled bookings are ignored' |
| 455 |
); |
456 |
); |
| 456 |
}; |
457 |
}; |
| 457 |
|
458 |
|
|
Lines 727-729
subtest 'store() tests' => sub {
Link Here
|
| 727 |
|
728 |
|
| 728 |
$schema->storage->txn_rollback; |
729 |
$schema->storage->txn_rollback; |
| 729 |
}; |
730 |
}; |
|
|
731 |
|
| 732 |
subtest '_select_optimal_item() tests' => sub { |
| 733 |
plan tests => 7; |
| 734 |
$schema->storage->txn_begin; |
| 735 |
|
| 736 |
my $patron = $builder->build_object( { class => "Koha::Patrons" } ); |
| 737 |
t::lib::Mocks::mock_userenv( { patron => $patron } ); |
| 738 |
|
| 739 |
my $biblio = $builder->build_sample_biblio; |
| 740 |
|
| 741 |
# Create 3 items with different future booking scenarios |
| 742 |
my $item1 = $builder->build_sample_item( { biblionumber => $biblio->biblionumber, bookable => 1 } ); |
| 743 |
my $item2 = $builder->build_sample_item( { biblionumber => $biblio->biblionumber, bookable => 1 } ); |
| 744 |
my $item3 = $builder->build_sample_item( { biblionumber => $biblio->biblionumber, bookable => 1 } ); |
| 745 |
|
| 746 |
# Current booking period |
| 747 |
my $start_date = dt_from_string->truncate( to => 'day' ); |
| 748 |
my $end_date = $start_date->clone->add( days => 5 ); |
| 749 |
|
| 750 |
# Test 1: With no items, should return undef |
| 751 |
my $test_booking = Koha::Booking->new( |
| 752 |
{ |
| 753 |
patron_id => $patron->borrowernumber, |
| 754 |
biblio_id => $biblio->biblionumber, |
| 755 |
pickup_library_id => $item1->homebranch, |
| 756 |
start_date => $start_date, |
| 757 |
end_date => $end_date |
| 758 |
} |
| 759 |
); |
| 760 |
|
| 761 |
my $empty_items = Koha::Items->search( { itemnumber => -1 } ); # Empty resultset |
| 762 |
my $optimal_item = $test_booking->_select_optimal_item($empty_items); |
| 763 |
is( $optimal_item, undef, 'Returns undef when no items available' ); |
| 764 |
|
| 765 |
# Test 2: With one item, should return that item |
| 766 |
my $single_items = Koha::Items->search( { itemnumber => $item1->itemnumber } ); |
| 767 |
$optimal_item = $test_booking->_select_optimal_item($single_items); |
| 768 |
is( $optimal_item->itemnumber, $item1->itemnumber, 'Returns the single available item' ); |
| 769 |
|
| 770 |
# Setup future bookings with different availability windows: |
| 771 |
# Item1: Next booking in 5 days (short future availability) |
| 772 |
# Item2: Next booking in 15 days (medium future availability) |
| 773 |
# Item3: No future bookings (longest future availability - 365 days) |
| 774 |
|
| 775 |
my $item1_future_start = $end_date->clone->add( days => 6 ); |
| 776 |
Koha::Booking->new( |
| 777 |
{ |
| 778 |
patron_id => $patron->borrowernumber, |
| 779 |
biblio_id => $biblio->biblionumber, |
| 780 |
item_id => $item1->itemnumber, |
| 781 |
pickup_library_id => $item1->homebranch, |
| 782 |
start_date => $item1_future_start, |
| 783 |
end_date => $item1_future_start->clone->add( days => 3 ) |
| 784 |
} |
| 785 |
)->store; |
| 786 |
|
| 787 |
my $item2_future_start = $end_date->clone->add( days => 16 ); |
| 788 |
Koha::Booking->new( |
| 789 |
{ |
| 790 |
patron_id => $patron->borrowernumber, |
| 791 |
biblio_id => $biblio->biblionumber, |
| 792 |
item_id => $item2->itemnumber, |
| 793 |
pickup_library_id => $item2->homebranch, |
| 794 |
start_date => $item2_future_start, |
| 795 |
end_date => $item2_future_start->clone->add( days => 3 ) |
| 796 |
} |
| 797 |
)->store; |
| 798 |
|
| 799 |
# Item3 has no future bookings |
| 800 |
|
| 801 |
# Test 3: Should select item3 (no future bookings = 365 days) |
| 802 |
my $all_items = Koha::Items->search( |
| 803 |
{ itemnumber => [ $item1->itemnumber, $item2->itemnumber, $item3->itemnumber ] }, |
| 804 |
{ order_by => { -asc => 'itemnumber' } } |
| 805 |
); |
| 806 |
$optimal_item = $test_booking->_select_optimal_item($all_items); |
| 807 |
is( $optimal_item->itemnumber, $item3->itemnumber, 'Selects item with longest future availability (no bookings)' ); |
| 808 |
|
| 809 |
# Test 4: If item3 is removed, should select item2 (15 days > 5 days) |
| 810 |
my $two_items = Koha::Items->search( |
| 811 |
{ itemnumber => [ $item1->itemnumber, $item2->itemnumber ] }, |
| 812 |
{ order_by => { -asc => 'itemnumber' } } |
| 813 |
); |
| 814 |
$optimal_item = $test_booking->_select_optimal_item($two_items); |
| 815 |
is( |
| 816 |
$optimal_item->itemnumber, $item2->itemnumber, |
| 817 |
'Selects item with longest future availability (15 days vs 5 days)' |
| 818 |
); |
| 819 |
|
| 820 |
# Test 5: Test with equal future availability - should return first item |
| 821 |
my $item4 = $builder->build_sample_item( { biblionumber => $biblio->biblionumber, bookable => 1 } ); |
| 822 |
my $item5 = $builder->build_sample_item( { biblionumber => $biblio->biblionumber, bookable => 1 } ); |
| 823 |
|
| 824 |
# Both items have booking starting 10 days after current booking ends |
| 825 |
my $equal_future_start = $end_date->clone->add( days => 11 ); |
| 826 |
Koha::Booking->new( |
| 827 |
{ |
| 828 |
patron_id => $patron->borrowernumber, |
| 829 |
biblio_id => $biblio->biblionumber, |
| 830 |
item_id => $item4->itemnumber, |
| 831 |
pickup_library_id => $item4->homebranch, |
| 832 |
start_date => $equal_future_start, |
| 833 |
end_date => $equal_future_start->clone->add( days => 3 ) |
| 834 |
} |
| 835 |
)->store; |
| 836 |
|
| 837 |
Koha::Booking->new( |
| 838 |
{ |
| 839 |
patron_id => $patron->borrowernumber, |
| 840 |
biblio_id => $biblio->biblionumber, |
| 841 |
item_id => $item5->itemnumber, |
| 842 |
pickup_library_id => $item5->homebranch, |
| 843 |
start_date => $equal_future_start, |
| 844 |
end_date => $equal_future_start->clone->add( days => 3 ) |
| 845 |
} |
| 846 |
)->store; |
| 847 |
|
| 848 |
my $equal_items = Koha::Items->search( |
| 849 |
{ itemnumber => [ $item4->itemnumber, $item5->itemnumber ] }, |
| 850 |
{ order_by => { -asc => 'itemnumber' } } |
| 851 |
); |
| 852 |
$optimal_item = $test_booking->_select_optimal_item($equal_items); |
| 853 |
ok( |
| 854 |
$optimal_item->itemnumber == $item4->itemnumber || $optimal_item->itemnumber == $item5->itemnumber, |
| 855 |
'Returns an item when future availability is equal' |
| 856 |
); |
| 857 |
|
| 858 |
# Test 6: Cancelled future bookings should not affect optimal selection |
| 859 |
my $item6 = $builder->build_sample_item( { biblionumber => $biblio->biblionumber, bookable => 1 } ); |
| 860 |
my $cancelled_future = $end_date->clone->add( days => 6 ); |
| 861 |
Koha::Booking->new( |
| 862 |
{ |
| 863 |
patron_id => $patron->borrowernumber, |
| 864 |
biblio_id => $biblio->biblionumber, |
| 865 |
item_id => $item6->itemnumber, |
| 866 |
pickup_library_id => $item6->homebranch, |
| 867 |
start_date => $cancelled_future, |
| 868 |
end_date => $cancelled_future->clone->add( days => 3 ), |
| 869 |
status => 'cancelled' |
| 870 |
} |
| 871 |
)->store; |
| 872 |
|
| 873 |
# Item6 should have 365 days availability since cancelled booking is ignored |
| 874 |
my $with_cancelled = Koha::Items->search( |
| 875 |
{ itemnumber => [ $item1->itemnumber, $item6->itemnumber ] }, |
| 876 |
{ order_by => { -asc => 'itemnumber' } } |
| 877 |
); |
| 878 |
$optimal_item = $test_booking->_select_optimal_item($with_cancelled); |
| 879 |
is( |
| 880 |
$optimal_item->itemnumber, $item6->itemnumber, |
| 881 |
'Selects item with cancelled future booking over item with active future booking' |
| 882 |
); |
| 883 |
|
| 884 |
# Test 7: Verify iterator is reset after selection |
| 885 |
$all_items->reset; |
| 886 |
$optimal_item = $test_booking->_select_optimal_item($all_items); |
| 887 |
my $count = $all_items->count; |
| 888 |
is( $count, 3, 'Iterator is properly reset after optimal selection' ); |
| 889 |
|
| 890 |
$schema->storage->txn_rollback; |
| 891 |
}; |
| 892 |
|
| 893 |
subtest '_assign_item_for_booking() with itemtype_id tests' => sub { |
| 894 |
plan tests => 6; |
| 895 |
$schema->storage->txn_begin; |
| 896 |
|
| 897 |
my $patron = $builder->build_object( { class => "Koha::Patrons" } ); |
| 898 |
t::lib::Mocks::mock_userenv( { patron => $patron } ); |
| 899 |
|
| 900 |
my $itemtype1 = $builder->build_object( { class => 'Koha::ItemTypes' } ); |
| 901 |
my $itemtype2 = $builder->build_object( { class => 'Koha::ItemTypes' } ); |
| 902 |
|
| 903 |
my $biblio = $builder->build_sample_biblio; |
| 904 |
|
| 905 |
# Create items of different types |
| 906 |
my $item1_type1 = |
| 907 |
$builder->build_sample_item( |
| 908 |
{ biblionumber => $biblio->biblionumber, bookable => 1, itype => $itemtype1->itemtype } ); |
| 909 |
my $item2_type1 = |
| 910 |
$builder->build_sample_item( |
| 911 |
{ biblionumber => $biblio->biblionumber, bookable => 1, itype => $itemtype1->itemtype } ); |
| 912 |
my $item3_type2 = |
| 913 |
$builder->build_sample_item( |
| 914 |
{ biblionumber => $biblio->biblionumber, bookable => 1, itype => $itemtype2->itemtype } ); |
| 915 |
|
| 916 |
my $start_date = dt_from_string->truncate( to => 'day' ); |
| 917 |
my $end_date = $start_date->clone->add( days => 5 ); |
| 918 |
|
| 919 |
# Test 1: Booking without item_id or itemtype_id should select any available item |
| 920 |
my $booking = Koha::Booking->new( |
| 921 |
{ |
| 922 |
patron_id => $patron->borrowernumber, |
| 923 |
biblio_id => $biblio->biblionumber, |
| 924 |
pickup_library_id => $item1_type1->homebranch, |
| 925 |
start_date => $start_date, |
| 926 |
end_date => $end_date |
| 927 |
} |
| 928 |
)->store; |
| 929 |
|
| 930 |
ok( $booking->item_id, 'Booking assigned an item when no item_id or itemtype_id specified' ); |
| 931 |
my $assigned_item = $booking->item; |
| 932 |
ok( |
| 933 |
$assigned_item->itemnumber == $item1_type1->itemnumber |
| 934 |
|| $assigned_item->itemnumber == $item2_type1->itemnumber |
| 935 |
|| $assigned_item->itemnumber == $item3_type2->itemnumber, |
| 936 |
'Assigned item is one of the available items' |
| 937 |
); |
| 938 |
|
| 939 |
# Test 2: Booking with itemtype_id should only select items of that type |
| 940 |
my $booking_type1 = Koha::Booking->new( |
| 941 |
{ |
| 942 |
patron_id => $patron->borrowernumber, |
| 943 |
biblio_id => $biblio->biblionumber, |
| 944 |
pickup_library_id => $item1_type1->homebranch, |
| 945 |
start_date => $start_date->clone->add( days => 10 ), |
| 946 |
end_date => $start_date->clone->add( days => 15 ) |
| 947 |
} |
| 948 |
); |
| 949 |
|
| 950 |
# Set transient itemtype filter (simulating API call) |
| 951 |
$booking_type1->set_itemtype_filter( $itemtype1->itemtype ); |
| 952 |
$booking_type1->store; |
| 953 |
|
| 954 |
ok( $booking_type1->item_id, 'Booking with itemtype_id assigned an item' ); |
| 955 |
is( |
| 956 |
$booking_type1->item->itype, $itemtype1->itemtype, |
| 957 |
'Assigned item matches the specified itemtype' |
| 958 |
); |
| 959 |
|
| 960 |
# Test 3: Should select optimal item from filtered itemtype |
| 961 |
# Create future bookings to test optimal selection within itemtype |
| 962 |
my $future_start = $end_date->clone->add( days => 20 ); |
| 963 |
|
| 964 |
# Item1 of type1 has a booking soon after (less future availability) |
| 965 |
Koha::Booking->new( |
| 966 |
{ |
| 967 |
patron_id => $patron->borrowernumber, |
| 968 |
biblio_id => $biblio->biblionumber, |
| 969 |
item_id => $item1_type1->itemnumber, |
| 970 |
pickup_library_id => $item1_type1->homebranch, |
| 971 |
start_date => $future_start->clone->add( days => 6 ), |
| 972 |
end_date => $future_start->clone->add( days => 10 ) |
| 973 |
} |
| 974 |
)->store; |
| 975 |
|
| 976 |
# Item2 of type1 has no future bookings (more future availability) |
| 977 |
# Item3 is type2, should not be considered |
| 978 |
|
| 979 |
my $optimal_booking = Koha::Booking->new( |
| 980 |
{ |
| 981 |
patron_id => $patron->borrowernumber, |
| 982 |
biblio_id => $biblio->biblionumber, |
| 983 |
pickup_library_id => $item1_type1->homebranch, |
| 984 |
start_date => $future_start, |
| 985 |
end_date => $future_start->clone->add( days => 3 ) |
| 986 |
} |
| 987 |
); |
| 988 |
$optimal_booking->set_itemtype_filter( $itemtype1->itemtype ); |
| 989 |
$optimal_booking->store; |
| 990 |
|
| 991 |
is( |
| 992 |
$optimal_booking->item->itemnumber, $item2_type1->itemnumber, |
| 993 |
'Optimal selection works within filtered itemtype (selects item with no future bookings)' |
| 994 |
); |
| 995 |
|
| 996 |
# Test 6: Exception when no items of specified type are available |
| 997 |
# Book both type1 items for an overlapping period |
| 998 |
my $conflict_start = |
| 999 |
dt_from_string->add( days => 200 )->truncate( to => 'day' ); # Use far future to avoid conflicts |
| 1000 |
my $conflict_end = $conflict_start->clone->add( days => 5 ); |
| 1001 |
|
| 1002 |
Koha::Booking->new( |
| 1003 |
{ |
| 1004 |
patron_id => $patron->borrowernumber, |
| 1005 |
biblio_id => $biblio->biblionumber, |
| 1006 |
item_id => $item1_type1->itemnumber, |
| 1007 |
pickup_library_id => $item1_type1->homebranch, |
| 1008 |
start_date => $conflict_start, |
| 1009 |
end_date => $conflict_end |
| 1010 |
} |
| 1011 |
)->store; |
| 1012 |
|
| 1013 |
Koha::Booking->new( |
| 1014 |
{ |
| 1015 |
patron_id => $patron->borrowernumber, |
| 1016 |
biblio_id => $biblio->biblionumber, |
| 1017 |
item_id => $item2_type1->itemnumber, |
| 1018 |
pickup_library_id => $item2_type1->homebranch, |
| 1019 |
start_date => $conflict_start, |
| 1020 |
end_date => $conflict_end |
| 1021 |
} |
| 1022 |
)->store; |
| 1023 |
|
| 1024 |
my $failing_booking = Koha::Booking->new( |
| 1025 |
{ |
| 1026 |
patron_id => $patron->borrowernumber, |
| 1027 |
biblio_id => $biblio->biblionumber, |
| 1028 |
pickup_library_id => $item1_type1->homebranch, |
| 1029 |
start_date => $conflict_start->clone->add( days => 2 ), |
| 1030 |
end_date => $conflict_end->clone->add( days => 2 ) |
| 1031 |
} |
| 1032 |
); |
| 1033 |
$failing_booking->set_itemtype_filter( $itemtype1->itemtype ); |
| 1034 |
|
| 1035 |
throws_ok { $failing_booking->store } 'Koha::Exceptions::Booking::Clash', |
| 1036 |
'Throws exception when no items of specified itemtype are available'; |
| 1037 |
|
| 1038 |
$schema->storage->txn_rollback; |
| 1039 |
}; |
| 1040 |
|
| 1041 |
subtest 'Integration test: Full optimal selection workflow' => sub { |
| 1042 |
plan tests => 5; |
| 1043 |
$schema->storage->txn_begin; |
| 1044 |
|
| 1045 |
my $patron = $builder->build_object( { class => "Koha::Patrons" } ); |
| 1046 |
t::lib::Mocks::mock_userenv( { patron => $patron } ); |
| 1047 |
|
| 1048 |
my $itemtype = $builder->build_object( { class => 'Koha::ItemTypes' } ); |
| 1049 |
my $biblio = $builder->build_sample_biblio; |
| 1050 |
|
| 1051 |
# Create 3 items of the same type with different future booking patterns |
| 1052 |
my $item_A = $builder->build_sample_item( |
| 1053 |
{ biblionumber => $biblio->biblionumber, bookable => 1, itype => $itemtype->itemtype } ); |
| 1054 |
my $item_B = $builder->build_sample_item( |
| 1055 |
{ biblionumber => $biblio->biblionumber, bookable => 1, itype => $itemtype->itemtype } ); |
| 1056 |
my $item_C = $builder->build_sample_item( |
| 1057 |
{ biblionumber => $biblio->biblionumber, bookable => 1, itype => $itemtype->itemtype } ); |
| 1058 |
|
| 1059 |
# Current booking window |
| 1060 |
my $start = dt_from_string->truncate( to => 'day' ); |
| 1061 |
my $end = $start->clone->add( days => 7 ); |
| 1062 |
|
| 1063 |
# Setup: Item A has booking in 5 days (short availability) |
| 1064 |
# Item B has booking in 20 days (long availability) |
| 1065 |
# Item C has booking in 10 days (medium availability) |
| 1066 |
my $future_A = $end->clone->add( days => 6 ); |
| 1067 |
Koha::Booking->new( |
| 1068 |
{ |
| 1069 |
patron_id => $patron->borrowernumber, |
| 1070 |
biblio_id => $biblio->biblionumber, |
| 1071 |
item_id => $item_A->itemnumber, |
| 1072 |
pickup_library_id => $item_A->homebranch, |
| 1073 |
start_date => $future_A, |
| 1074 |
end_date => $future_A->clone->add( days => 3 ) |
| 1075 |
} |
| 1076 |
)->store; |
| 1077 |
|
| 1078 |
my $future_B = $end->clone->add( days => 21 ); |
| 1079 |
Koha::Booking->new( |
| 1080 |
{ |
| 1081 |
patron_id => $patron->borrowernumber, |
| 1082 |
biblio_id => $biblio->biblionumber, |
| 1083 |
item_id => $item_B->itemnumber, |
| 1084 |
pickup_library_id => $item_B->homebranch, |
| 1085 |
start_date => $future_B, |
| 1086 |
end_date => $future_B->clone->add( days => 3 ) |
| 1087 |
} |
| 1088 |
)->store; |
| 1089 |
|
| 1090 |
my $future_C = $end->clone->add( days => 11 ); |
| 1091 |
Koha::Booking->new( |
| 1092 |
{ |
| 1093 |
patron_id => $patron->borrowernumber, |
| 1094 |
biblio_id => $biblio->biblionumber, |
| 1095 |
item_id => $item_C->itemnumber, |
| 1096 |
pickup_library_id => $item_C->homebranch, |
| 1097 |
start_date => $future_C, |
| 1098 |
end_date => $future_C->clone->add( days => 3 ) |
| 1099 |
} |
| 1100 |
)->store; |
| 1101 |
|
| 1102 |
# Test 1: First "any item" booking should select Item B (longest availability: 20 days) |
| 1103 |
my $booking1 = Koha::Booking->new( |
| 1104 |
{ |
| 1105 |
patron_id => $patron->borrowernumber, |
| 1106 |
biblio_id => $biblio->biblionumber, |
| 1107 |
pickup_library_id => $item_A->homebranch, |
| 1108 |
start_date => $start, |
| 1109 |
end_date => $end |
| 1110 |
} |
| 1111 |
); |
| 1112 |
$booking1->set_itemtype_filter( $itemtype->itemtype ); |
| 1113 |
$booking1->store; |
| 1114 |
|
| 1115 |
is( |
| 1116 |
$booking1->item_id, $item_B->itemnumber, |
| 1117 |
'First booking selects item B (longest future availability: 20 days)' |
| 1118 |
); |
| 1119 |
|
| 1120 |
# Test 2: Second booking should select Item C (medium availability: 10 days) |
| 1121 |
# Item B is now booked, Item A still has shortest (5 days) |
| 1122 |
my $booking2 = Koha::Booking->new( |
| 1123 |
{ |
| 1124 |
patron_id => $patron->borrowernumber, |
| 1125 |
biblio_id => $biblio->biblionumber, |
| 1126 |
pickup_library_id => $item_A->homebranch, |
| 1127 |
start_date => $start, |
| 1128 |
end_date => $end |
| 1129 |
} |
| 1130 |
); |
| 1131 |
$booking2->set_itemtype_filter( $itemtype->itemtype ); |
| 1132 |
$booking2->store; |
| 1133 |
|
| 1134 |
is( $booking2->item_id, $item_C->itemnumber, 'Second booking selects item C (next longest: 10 days)' ); |
| 1135 |
|
| 1136 |
# Test 3: Third booking should select Item A (only one left, 5 days availability) |
| 1137 |
my $booking3 = Koha::Booking->new( |
| 1138 |
{ |
| 1139 |
patron_id => $patron->borrowernumber, |
| 1140 |
biblio_id => $biblio->biblionumber, |
| 1141 |
pickup_library_id => $item_A->homebranch, |
| 1142 |
start_date => $start, |
| 1143 |
end_date => $end |
| 1144 |
} |
| 1145 |
); |
| 1146 |
$booking3->set_itemtype_filter( $itemtype->itemtype ); |
| 1147 |
$booking3->store; |
| 1148 |
|
| 1149 |
is( $booking3->item_id, $item_A->itemnumber, 'Third booking selects item A (only remaining item)' ); |
| 1150 |
|
| 1151 |
# Test 4: Fourth booking should fail (all items booked for this period) |
| 1152 |
my $booking4 = Koha::Booking->new( |
| 1153 |
{ |
| 1154 |
patron_id => $patron->borrowernumber, |
| 1155 |
biblio_id => $biblio->biblionumber, |
| 1156 |
pickup_library_id => $item_A->homebranch, |
| 1157 |
start_date => $start, |
| 1158 |
end_date => $end |
| 1159 |
} |
| 1160 |
); |
| 1161 |
$booking4->set_itemtype_filter( $itemtype->itemtype ); |
| 1162 |
|
| 1163 |
throws_ok { $booking4->store } 'Koha::Exceptions::Booking::Clash', |
| 1164 |
'Fourth booking fails when all items are already booked'; |
| 1165 |
|
| 1166 |
# Test 5: Verify the algorithm preserved items optimally |
| 1167 |
# Item A (shortest availability) was selected last, preserving it for bookings that specifically need it |
| 1168 |
# This demonstrates that the optimal selection algorithm works as intended: |
| 1169 |
# - Longest availability items are consumed first |
| 1170 |
# - Shortest availability items are preserved for when they're the only option |
| 1171 |
# - This maximizes overall system booking capacity |
| 1172 |
|
| 1173 |
my @booking_order = ( $booking1->item_id, $booking2->item_id, $booking3->item_id ); |
| 1174 |
my @expected_order = ( $item_B->itemnumber, $item_C->itemnumber, $item_A->itemnumber ); |
| 1175 |
|
| 1176 |
is_deeply( |
| 1177 |
\@booking_order, \@expected_order, |
| 1178 |
'Items were selected in optimal order: longest availability first (B->C->A)' |
| 1179 |
); |
| 1180 |
|
| 1181 |
$schema->storage->txn_rollback; |
| 1182 |
}; |