View | Details | Raw Unified | Return to bug 40134
Collapse All | Expand All

(-)a/t/db_dependent/Koha/Booking.t (-4 / +457 lines)
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
};
(-)a/t/db_dependent/api/v1/bookings.t (-6 / +140 lines)
Lines 18-24 Link Here
18
use Modern::Perl;
18
use Modern::Perl;
19
19
20
use Test::NoWarnings;
20
use Test::NoWarnings;
21
use Test::More tests => 7;
21
use Test::More tests => 8;
22
use Test::Mojo;
22
use Test::Mojo;
23
use Test::Warn;
23
use Test::Warn;
24
24
Lines 231-237 subtest 'add() tests' => sub { Link Here
231
    my $pickup_library = $builder->build_object( { class => 'Koha::Libraries' } );
231
    my $pickup_library = $builder->build_object( { class => 'Koha::Libraries' } );
232
    my $booking        = {
232
    my $booking        = {
233
        biblio_id         => $biblio->id,
233
        biblio_id         => $biblio->id,
234
        item_id           => undef,
234
        item_id           => $item1->itemnumber,
235
        pickup_library_id => $pickup_library->branchcode,
235
        pickup_library_id => $pickup_library->branchcode,
236
        patron_id         => $patron->id,
236
        patron_id         => $patron->id,
237
        start_date        => output_pref( { dateformat => "rfc3339", dt => dt_from_string->add( days => 2 ) } ),
237
        start_date        => output_pref( { dateformat => "rfc3339", dt => dt_from_string->add( days => 2 ) } ),
Lines 265-271 subtest 'add() tests' => sub { Link Here
265
    $t->post_ok( "//$userid:$password@/api/v1/bookings" => json => $booking )->status_is(400)->json_has('/errors');
265
    $t->post_ok( "//$userid:$password@/api/v1/bookings" => json => $booking )->status_is(400)->json_has('/errors');
266
266
267
    # Authorized attempt to create with existing id
267
    # Authorized attempt to create with existing id
268
    # Use different dates to avoid triggering clash detection (we want to test duplicate ID handling)
268
    $booking->{booking_id} = $booking_id;
269
    $booking->{booking_id} = $booking_id;
270
    $booking->{start_date} = output_pref( { dateformat => "rfc3339", dt => dt_from_string->add( days => 10 ) } );
271
    $booking->{end_date}   = output_pref( { dateformat => "rfc3339", dt => dt_from_string->add( days => 14 ) } );
269
    warnings_like {
272
    warnings_like {
270
        $t->post_ok( "//$userid:$password@/api/v1/bookings" => json => $booking )->status_is(409)
273
        $t->post_ok( "//$userid:$password@/api/v1/bookings" => json => $booking )->status_is(409)
271
            ->json_is( "/error" => "Duplicate booking_id" );
274
            ->json_is( "/error" => "Duplicate booking_id" );
Lines 327-333 subtest 'update() tests' => sub { Link Here
327
330
328
    # Attempt partial update on a PUT
331
    # Attempt partial update on a PUT
329
    my $booking_with_missing_field = {
332
    my $booking_with_missing_field = {
330
        item_id           => undef,
333
        item_id           => $item->itemnumber,
331
        patron_id         => $patron->id,
334
        patron_id         => $patron->id,
332
        pickup_library_id => $pickup_library->branchcode,
335
        pickup_library_id => $pickup_library->branchcode,
333
        start_date        => output_pref( { dateformat => "rfc3339", dt => dt_from_string->add( days => 2 ) } ),
336
        start_date        => output_pref( { dateformat => "rfc3339", dt => dt_from_string->add( days => 2 ) } ),
Lines 340-346 subtest 'update() tests' => sub { Link Here
340
    # Full object update on PUT
343
    # Full object update on PUT
341
    my $booking_with_updated_field = {
344
    my $booking_with_updated_field = {
342
        biblio_id         => $biblio->id,
345
        biblio_id         => $biblio->id,
343
        item_id           => undef,
346
        item_id           => $item->itemnumber,
344
        pickup_library_id => $pickup_library->branchcode,
347
        pickup_library_id => $pickup_library->branchcode,
345
        patron_id         => $patron->id,
348
        patron_id         => $patron->id,
346
        start_date        => output_pref( { dateformat => "rfc3339", dt => dt_from_string->add( days => 2 ) } ),
349
        start_date        => output_pref( { dateformat => "rfc3339", dt => dt_from_string->add( days => 2 ) } ),
Lines 354-360 subtest 'update() tests' => sub { Link Here
354
    my $booking_with_invalid_field = {
357
    my $booking_with_invalid_field = {
355
        blah              => "Booking Blah",
358
        blah              => "Booking Blah",
356
        biblio_id         => $biblio->id,
359
        biblio_id         => $biblio->id,
357
        item_id           => undef,
360
        item_id           => $item->itemnumber,
358
        pickup_library_id => $pickup_library->branchcode,
361
        pickup_library_id => $pickup_library->branchcode,
359
        patron_id         => $patron->id,
362
        patron_id         => $patron->id,
360
        start_date        => output_pref( { dateformat => "rfc3339", dt => dt_from_string->add( days => 2 ) } ),
363
        start_date        => output_pref( { dateformat => "rfc3339", dt => dt_from_string->add( days => 2 ) } ),
Lines 502-504 subtest 'patch() tests' => sub { Link Here
502
505
503
    $schema->storage->txn_rollback;
506
    $schema->storage->txn_rollback;
504
};
507
};
505
- 
508
509
subtest 'add() with itemtype_id tests' => sub {
510
511
    plan tests => 16;
512
513
    $schema->storage->txn_begin;
514
515
    my $librarian = $builder->build_object(
516
        {
517
            class => 'Koha::Patrons',
518
            value => { flags => 0 }     # no additional permissions
519
        }
520
    );
521
    $builder->build(
522
        {
523
            source => 'UserPermission',
524
            value  => {
525
                borrowernumber => $librarian->borrowernumber,
526
                module_bit     => 1,
527
                code           => 'manage_bookings',
528
            },
529
        }
530
    );
531
    my $password = 'thePassword123';
532
    $librarian->set_password( { password => $password, skip_validation => 1 } );
533
    my $userid = $librarian->userid;
534
535
    my $patron = $builder->build_object(
536
        {
537
            class => 'Koha::Patrons',
538
            value => { flags => 0 }
539
        }
540
    );
541
542
    $patron->set_password( { password => $password, skip_validation => 1 } );
543
544
    # Create itemtype and items
545
    my $itemtype = $builder->build_object( { class => 'Koha::ItemTypes' } );
546
    my $biblio   = $builder->build_sample_biblio;
547
    my $item1    = $builder->build_sample_item(
548
        {
549
            biblionumber => $biblio->id,
550
            bookable     => 1,
551
            itype        => $itemtype->itemtype
552
        }
553
    );
554
    my $item2 = $builder->build_sample_item(
555
        {
556
            biblionumber => $biblio->id,
557
            bookable     => 1,
558
            itype        => $itemtype->itemtype
559
        }
560
    );
561
562
    my $pickup_library = $builder->build_object( { class => 'Koha::Libraries' } );
563
564
    # Test 1: Booking with itemtype_id instead of item_id should work
565
    my $booking_with_itemtype = {
566
        biblio_id         => $biblio->id,
567
        itemtype_id       => $itemtype->itemtype,
568
        pickup_library_id => $pickup_library->branchcode,
569
        patron_id         => $patron->id,
570
        start_date        => output_pref( { dateformat => "rfc3339", dt => dt_from_string->add( days => 2 ) } ),
571
        end_date          => output_pref( { dateformat => "rfc3339", dt => dt_from_string->add( days => 6 ) } ),
572
    };
573
574
    my $tx =
575
        $t->post_ok( "//$userid:$password@/api/v1/bookings" => json => $booking_with_itemtype )
576
        ->status_is( 201, 'Created booking with itemtype_id' )->json_has( '/item_id', 'Server assigned an item_id' )
577
        ->json_is( '/biblio_id' => $biblio->id );
578
579
    my $assigned_item_id = $tx->tx->res->json->{item_id};
580
    ok(
581
        $assigned_item_id == $item1->itemnumber || $assigned_item_id == $item2->itemnumber,
582
        'Assigned item is one of the items of the specified itemtype'
583
    );
584
585
    # Test 2: Booking with both item_id and itemtype_id should fail
586
    my $booking_with_both = {
587
        biblio_id         => $biblio->id,
588
        item_id           => $item1->itemnumber,
589
        itemtype_id       => $itemtype->itemtype,
590
        pickup_library_id => $pickup_library->branchcode,
591
        patron_id         => $patron->id,
592
        start_date        => output_pref( { dateformat => "rfc3339", dt => dt_from_string->add( days => 10 ) } ),
593
        end_date          => output_pref( { dateformat => "rfc3339", dt => dt_from_string->add( days => 14 ) } ),
594
    };
595
596
    $t->post_ok( "//$userid:$password@/api/v1/bookings" => json => $booking_with_both )->status_is(400)
597
        ->json_is( '/error' => 'Cannot specify both item_id and itemtype_id' );
598
599
    # Test 3: Booking with neither item_id nor itemtype_id should fail
600
    my $booking_with_neither = {
601
        biblio_id         => $biblio->id,
602
        pickup_library_id => $pickup_library->branchcode,
603
        patron_id         => $patron->id,
604
        start_date        => output_pref( { dateformat => "rfc3339", dt => dt_from_string->add( days => 20 ) } ),
605
        end_date          => output_pref( { dateformat => "rfc3339", dt => dt_from_string->add( days => 24 ) } ),
606
    };
607
608
    $t->post_ok( "//$userid:$password@/api/v1/bookings" => json => $booking_with_neither )->status_is(400)
609
        ->json_is( '/error' => 'Either item_id or itemtype_id must be provided' );
610
611
    # Test 4: Verify optimal selection - book all items, then try to book again
612
    # Book item2 for the same period as the first booking
613
    my $booking_item2 = {
614
        biblio_id         => $biblio->id,
615
        item_id           => $item2->itemnumber,
616
        pickup_library_id => $pickup_library->branchcode,
617
        patron_id         => $patron->id,
618
        start_date        => output_pref( { dateformat => "rfc3339", dt => dt_from_string->add( days => 2 ) } ),
619
        end_date          => output_pref( { dateformat => "rfc3339", dt => dt_from_string->add( days => 6 ) } ),
620
    };
621
622
    $t->post_ok( "//$userid:$password@/api/v1/bookings" => json => $booking_item2 )->status_is(201);
623
624
    # Now try to create another booking with itemtype_id for the same period
625
    # Should fail since both items are booked
626
    my $booking_should_fail = {
627
        biblio_id         => $biblio->id,
628
        itemtype_id       => $itemtype->itemtype,
629
        pickup_library_id => $pickup_library->branchcode,
630
        patron_id         => $patron->id,
631
        start_date        => output_pref( { dateformat => "rfc3339", dt => dt_from_string->add( days => 3 ) } ),
632
        end_date          => output_pref( { dateformat => "rfc3339", dt => dt_from_string->add( days => 5 ) } ),
633
    };
634
635
    $t->post_ok( "//$userid:$password@/api/v1/bookings" => json => $booking_should_fail )->status_is(400)
636
        ->json_is( '/error' => 'Booking would conflict' );
637
638
    $schema->storage->txn_rollback;
639
};

Return to bug 40134