|
Lines 20-26
Link Here
|
| 20 |
use Modern::Perl; |
20 |
use Modern::Perl; |
| 21 |
|
21 |
|
| 22 |
use Test::NoWarnings; |
22 |
use Test::NoWarnings; |
| 23 |
use Test::More tests => 18; |
23 |
use Test::More tests => 21; |
| 24 |
|
24 |
|
| 25 |
use Test::Exception; |
25 |
use Test::Exception; |
| 26 |
use Test::MockModule; |
26 |
use Test::MockModule; |
|
Lines 32-37
use t::lib::Mocks;
Link Here
|
| 32 |
use C4::Reserves qw(AddReserve); |
32 |
use C4::Reserves qw(AddReserve); |
| 33 |
|
33 |
|
| 34 |
use Koha::ActionLogs; |
34 |
use Koha::ActionLogs; |
|
|
35 |
use Koha::CirculationRules; |
| 35 |
use Koha::DateUtils qw(dt_from_string); |
36 |
use Koha::DateUtils qw(dt_from_string); |
| 36 |
use Koha::Holds; |
37 |
use Koha::Holds; |
| 37 |
use Koha::Libraries; |
38 |
use Koha::Libraries; |
|
Lines 115-133
subtest 'fill() tests' => sub {
Link Here
|
| 115 |
|
116 |
|
| 116 |
my $fee = 15; |
117 |
my $fee = 15; |
| 117 |
|
118 |
|
| 118 |
my $category = $builder->build_object( |
119 |
my $category = $builder->build_object( { class => 'Koha::Patron::Categories' } ); |
| 119 |
{ |
120 |
my $patron = $builder->build_object( |
| 120 |
class => 'Koha::Patron::Categories', |
|
|
| 121 |
value => { reservefee => $fee } |
| 122 |
} |
| 123 |
); |
| 124 |
my $patron = $builder->build_object( |
| 125 |
{ |
121 |
{ |
| 126 |
class => 'Koha::Patrons', |
122 |
class => 'Koha::Patrons', |
| 127 |
value => { categorycode => $category->id } |
123 |
value => { categorycode => $category->id } |
| 128 |
} |
124 |
} |
| 129 |
); |
125 |
); |
| 130 |
my $manager = $builder->build_object( { class => 'Koha::Patrons' } ); |
126 |
my $manager = $builder->build_object( { class => 'Koha::Patrons' } ); |
|
|
127 |
my $library = $builder->build_object( { class => 'Koha::Libraries' } ); |
| 128 |
|
| 129 |
# Set up circulation rules for hold fees |
| 130 |
Koha::CirculationRules->set_rules( |
| 131 |
{ |
| 132 |
branchcode => undef, |
| 133 |
categorycode => $category->id, |
| 134 |
itemtype => undef, |
| 135 |
rules => { |
| 136 |
hold_fee => $fee, |
| 137 |
} |
| 138 |
} |
| 139 |
); |
| 131 |
|
140 |
|
| 132 |
my $title = 'Do what you want'; |
141 |
my $title = 'Do what you want'; |
| 133 |
my $biblio = $builder->build_sample_biblio( { title => $title } ); |
142 |
my $biblio = $builder->build_sample_biblio( { title => $title } ); |
|
Lines 139-144
subtest 'fill() tests' => sub {
Link Here
|
| 139 |
biblionumber => $biblio->id, |
148 |
biblionumber => $biblio->id, |
| 140 |
borrowernumber => $patron->id, |
149 |
borrowernumber => $patron->id, |
| 141 |
itemnumber => $item->id, |
150 |
itemnumber => $item->id, |
|
|
151 |
branchcode => $library->branchcode, |
| 142 |
timestamp => dt_from_string('2021-06-25 14:05:35'), |
152 |
timestamp => dt_from_string('2021-06-25 14:05:35'), |
| 143 |
priority => 10, |
153 |
priority => 10, |
| 144 |
} |
154 |
} |
|
Lines 167-178
subtest 'fill() tests' => sub {
Link Here
|
| 167 |
|
177 |
|
| 168 |
subtest 'item_id parameter' => sub { |
178 |
subtest 'item_id parameter' => sub { |
| 169 |
plan tests => 1; |
179 |
plan tests => 1; |
| 170 |
$category->reservefee(0)->store; # do not disturb later accounts |
180 |
|
|
|
181 |
# Clear hold fee rule to not disturb later accounts |
| 182 |
Koha::CirculationRules->set_rules( |
| 183 |
{ |
| 184 |
branchcode => undef, |
| 185 |
categorycode => $category->id, |
| 186 |
itemtype => undef, |
| 187 |
rules => { |
| 188 |
hold_fee => 0, |
| 189 |
} |
| 190 |
} |
| 191 |
); |
| 171 |
$hold = $builder->build_object( |
192 |
$hold = $builder->build_object( |
| 172 |
{ |
193 |
{ |
| 173 |
class => 'Koha::Holds', |
194 |
class => 'Koha::Holds', |
| 174 |
value => |
195 |
value => { |
| 175 |
{ biblionumber => $biblio->id, borrowernumber => $patron->id, itemnumber => undef, priority => 1 } |
196 |
biblionumber => $biblio->id, borrowernumber => $patron->id, itemnumber => undef, priority => 1, |
|
|
197 |
branchcode => $library->branchcode |
| 198 |
} |
| 176 |
} |
199 |
} |
| 177 |
); |
200 |
); |
| 178 |
|
201 |
|
|
Lines 181-187
subtest 'fill() tests' => sub {
Link Here
|
| 181 |
$old_hold = Koha::Old::Holds->find( $hold->id ); |
204 |
$old_hold = Koha::Old::Holds->find( $hold->id ); |
| 182 |
is( $old_hold->itemnumber, $item->itemnumber, 'The itemnumber has been saved in old_reserves by fill' ); |
205 |
is( $old_hold->itemnumber, $item->itemnumber, 'The itemnumber has been saved in old_reserves by fill' ); |
| 183 |
$old_hold->delete; |
206 |
$old_hold->delete; |
| 184 |
$category->reservefee($fee)->store; # restore |
207 |
|
|
|
208 |
# Restore hold fee rule |
| 209 |
Koha::CirculationRules->set_rules( |
| 210 |
{ |
| 211 |
branchcode => undef, |
| 212 |
categorycode => $category->id, |
| 213 |
itemtype => undef, |
| 214 |
rules => { |
| 215 |
hold_fee => $fee, |
| 216 |
} |
| 217 |
} |
| 218 |
); |
| 185 |
}; |
219 |
}; |
| 186 |
|
220 |
|
| 187 |
subtest 'fee applied tests' => sub { |
221 |
subtest 'fee applied tests' => sub { |
|
Lines 1847-1849
subtest 'move_hold() tests' => sub {
Link Here
|
| 1847 |
|
1881 |
|
| 1848 |
$schema->storage->txn_rollback; |
1882 |
$schema->storage->txn_rollback; |
| 1849 |
}; |
1883 |
}; |
|
|
1884 |
|
| 1885 |
subtest 'calculate_hold_fee() tests' => sub { |
| 1886 |
plan tests => 12; |
| 1887 |
|
| 1888 |
$schema->storage->txn_begin; |
| 1889 |
|
| 1890 |
# Create patron category and patron |
| 1891 |
my $cat = $builder->build( { source => 'Category', value => { categorycode => 'XYZ1' } } ); |
| 1892 |
my $patron1 = $builder->build_object( |
| 1893 |
{ |
| 1894 |
class => 'Koha::Patrons', |
| 1895 |
value => { categorycode => 'XYZ1' } |
| 1896 |
} |
| 1897 |
); |
| 1898 |
my $patron2 = $builder->build_object( |
| 1899 |
{ |
| 1900 |
class => 'Koha::Patrons', |
| 1901 |
value => { categorycode => 'XYZ1' } |
| 1902 |
} |
| 1903 |
); |
| 1904 |
|
| 1905 |
# Create itemtypes with different fees |
| 1906 |
my $itemtype1 = $builder->build( { source => 'Itemtype' } ); |
| 1907 |
my $itemtype2 = $builder->build( { source => 'Itemtype' } ); |
| 1908 |
my $itemtype3 = $builder->build( { source => 'Itemtype' } ); |
| 1909 |
|
| 1910 |
# Set up circulation rules with different hold fees for different itemtypes |
| 1911 |
Koha::CirculationRules->set_rules( |
| 1912 |
{ |
| 1913 |
branchcode => undef, |
| 1914 |
categorycode => 'XYZ1', |
| 1915 |
itemtype => $itemtype1->{itemtype}, |
| 1916 |
rules => { hold_fee => 1.00, reservesallowed => 10 } |
| 1917 |
} |
| 1918 |
); |
| 1919 |
Koha::CirculationRules->set_rules( |
| 1920 |
{ |
| 1921 |
branchcode => undef, |
| 1922 |
categorycode => 'XYZ1', |
| 1923 |
itemtype => $itemtype2->{itemtype}, |
| 1924 |
rules => { hold_fee => 3.00, reservesallowed => 10 } |
| 1925 |
} |
| 1926 |
); |
| 1927 |
Koha::CirculationRules->set_rules( |
| 1928 |
{ |
| 1929 |
branchcode => undef, |
| 1930 |
categorycode => 'XYZ1', |
| 1931 |
itemtype => $itemtype3->{itemtype}, |
| 1932 |
rules => { hold_fee => 2.00, reservesallowed => 10 } |
| 1933 |
} |
| 1934 |
); |
| 1935 |
|
| 1936 |
# Set generic rules for permission checking |
| 1937 |
Koha::CirculationRules->set_rules( |
| 1938 |
{ |
| 1939 |
branchcode => undef, |
| 1940 |
categorycode => undef, |
| 1941 |
itemtype => undef, |
| 1942 |
rules => { reservesallowed => 10, holds_per_record => 10, holds_per_day => 10 } |
| 1943 |
} |
| 1944 |
); |
| 1945 |
|
| 1946 |
my $biblio = $builder->build_sample_biblio(); |
| 1947 |
|
| 1948 |
# Create multiple items with different itemtypes and fees |
| 1949 |
my $item1 = $builder->build_sample_item( |
| 1950 |
{ |
| 1951 |
biblionumber => $biblio->biblionumber, |
| 1952 |
itype => $itemtype1->{itemtype}, |
| 1953 |
notforloan => 0, |
| 1954 |
} |
| 1955 |
); |
| 1956 |
my $item2 = $builder->build_sample_item( |
| 1957 |
{ |
| 1958 |
biblionumber => $biblio->biblionumber, |
| 1959 |
itype => $itemtype2->{itemtype}, |
| 1960 |
notforloan => 0, |
| 1961 |
} |
| 1962 |
); |
| 1963 |
my $item3 = $builder->build_sample_item( |
| 1964 |
{ |
| 1965 |
biblionumber => $biblio->biblionumber, |
| 1966 |
itype => $itemtype3->{itemtype}, |
| 1967 |
notforloan => 0, |
| 1968 |
} |
| 1969 |
); |
| 1970 |
|
| 1971 |
# Test 1: Item-level hold calculates fee correctly |
| 1972 |
my $item_hold = $builder->build_object( |
| 1973 |
{ |
| 1974 |
class => 'Koha::Holds', |
| 1975 |
value => { |
| 1976 |
borrowernumber => $patron1->borrowernumber, |
| 1977 |
biblionumber => $biblio->biblionumber, |
| 1978 |
itemnumber => $item2->itemnumber, # Use item2 with 3.00 fee |
| 1979 |
} |
| 1980 |
} |
| 1981 |
); |
| 1982 |
|
| 1983 |
my $fee = $item_hold->calculate_hold_fee(); |
| 1984 |
is( $fee, 3.00, 'Item-level hold calculates fee correctly' ); |
| 1985 |
|
| 1986 |
# Create title-level hold for comprehensive testing |
| 1987 |
my $title_hold = $builder->build_object( |
| 1988 |
{ |
| 1989 |
class => 'Koha::Holds', |
| 1990 |
value => { |
| 1991 |
borrowernumber => $patron2->borrowernumber, |
| 1992 |
biblionumber => $biblio->biblionumber, |
| 1993 |
itemnumber => undef, |
| 1994 |
} |
| 1995 |
} |
| 1996 |
); |
| 1997 |
|
| 1998 |
# Test 2: Default 'highest' strategy should return 3.00 (highest fee) |
| 1999 |
t::lib::Mocks::mock_preference( 'TitleHoldFeeStrategy', 'highest' ); |
| 2000 |
$fee = $title_hold->calculate_hold_fee(); |
| 2001 |
is( $fee, 3.00, 'Title-level hold: highest strategy returns highest fee (3.00)' ); |
| 2002 |
|
| 2003 |
# Test 3: 'lowest' strategy should return 1.00 (lowest fee) |
| 2004 |
t::lib::Mocks::mock_preference( 'TitleHoldFeeStrategy', 'lowest' ); |
| 2005 |
$fee = $title_hold->calculate_hold_fee(); |
| 2006 |
is( $fee, 1.00, 'Title-level hold: lowest strategy returns lowest fee (1.00)' ); |
| 2007 |
|
| 2008 |
# Test 4: 'most_common' strategy with unique fees should return highest |
| 2009 |
t::lib::Mocks::mock_preference( 'TitleHoldFeeStrategy', 'most_common' ); |
| 2010 |
$fee = $title_hold->calculate_hold_fee(); |
| 2011 |
is( $fee, 3.00, 'Title-level hold: most_common strategy with unique fees returns highest fee (3.00)' ); |
| 2012 |
|
| 2013 |
# Test 5: Add another item with same fee to test most_common properly |
| 2014 |
my $item4 = $builder->build_sample_item( |
| 2015 |
{ |
| 2016 |
biblionumber => $biblio->biblionumber, |
| 2017 |
itype => $itemtype1->{itemtype}, |
| 2018 |
notforloan => 0, |
| 2019 |
} |
| 2020 |
); |
| 2021 |
$fee = $title_hold->calculate_hold_fee(); |
| 2022 |
is( $fee, 1.00, 'Title-level hold: most_common strategy returns most common fee (1.00 - 2 items)' ); |
| 2023 |
|
| 2024 |
# Test 6: Unknown/invalid strategy defaults to 'highest' |
| 2025 |
t::lib::Mocks::mock_preference( 'TitleHoldFeeStrategy', 'invalid_strategy' ); |
| 2026 |
$fee = $title_hold->calculate_hold_fee(); |
| 2027 |
is( $fee, 3.00, 'Title-level hold: invalid strategy defaults to highest fee (3.00)' ); |
| 2028 |
|
| 2029 |
# Test 7: Empty preference defaults to 'highest' |
| 2030 |
t::lib::Mocks::mock_preference( 'TitleHoldFeeStrategy', '' ); |
| 2031 |
$fee = $title_hold->calculate_hold_fee(); |
| 2032 |
is( $fee, 3.00, 'Title-level hold: empty strategy preference defaults to highest fee (3.00)' ); |
| 2033 |
|
| 2034 |
# Test 8: No holdable items returns 0 |
| 2035 |
# Make all items not holdable |
| 2036 |
$item1->notforloan(1)->store; |
| 2037 |
$item2->notforloan(1)->store; |
| 2038 |
$item3->notforloan(1)->store; |
| 2039 |
$item4->notforloan(1)->store; |
| 2040 |
|
| 2041 |
$fee = $title_hold->calculate_hold_fee(); |
| 2042 |
is( $fee, 0, 'Title-level hold: no holdable items returns 0 fee' ); |
| 2043 |
|
| 2044 |
# Test 9: Mix of holdable and non-holdable items (highest) |
| 2045 |
# Make some items holdable again |
| 2046 |
$item2->notforloan(0)->store; # Fee: 3.00 |
| 2047 |
$item3->notforloan(0)->store; # Fee: 2.00 |
| 2048 |
|
| 2049 |
t::lib::Mocks::mock_preference( 'TitleHoldFeeStrategy', 'highest' ); |
| 2050 |
$fee = $title_hold->calculate_hold_fee(); |
| 2051 |
is( $fee, 3.00, 'Title-level hold: highest strategy with mixed holdable items returns 3.00' ); |
| 2052 |
|
| 2053 |
# Test 10: Mix of holdable and non-holdable items (lowest) |
| 2054 |
t::lib::Mocks::mock_preference( 'TitleHoldFeeStrategy', 'lowest' ); |
| 2055 |
$fee = $title_hold->calculate_hold_fee(); |
| 2056 |
is( $fee, 2.00, 'Title-level hold: lowest strategy with mixed holdable items returns 2.00' ); |
| 2057 |
|
| 2058 |
# Test 11: Items with 0 fee |
| 2059 |
my $itemtype_free = $builder->build( { source => 'Itemtype' } ); |
| 2060 |
Koha::CirculationRules->set_rules( |
| 2061 |
{ |
| 2062 |
branchcode => undef, |
| 2063 |
categorycode => 'XYZ1', |
| 2064 |
itemtype => $itemtype_free->{itemtype}, |
| 2065 |
rules => { hold_fee => 0, reservesallowed => 10 } |
| 2066 |
} |
| 2067 |
); |
| 2068 |
|
| 2069 |
my $item_free = $builder->build_sample_item( |
| 2070 |
{ |
| 2071 |
biblionumber => $biblio->biblionumber, |
| 2072 |
itype => $itemtype_free->{itemtype}, |
| 2073 |
notforloan => 0, |
| 2074 |
} |
| 2075 |
); |
| 2076 |
|
| 2077 |
t::lib::Mocks::mock_preference( 'TitleHoldFeeStrategy', 'lowest' ); |
| 2078 |
$fee = $title_hold->calculate_hold_fee(); |
| 2079 |
is( $fee, 0, 'Title-level hold: lowest strategy with free item returns 0' ); |
| 2080 |
|
| 2081 |
# Test 12: All items have same fee |
| 2082 |
# Set all holdable items to same fee |
| 2083 |
Koha::CirculationRules->set_rules( |
| 2084 |
{ |
| 2085 |
branchcode => undef, |
| 2086 |
categorycode => 'XYZ1', |
| 2087 |
itemtype => $itemtype2->{itemtype}, |
| 2088 |
rules => { hold_fee => 2.50, reservesallowed => 10 } |
| 2089 |
} |
| 2090 |
); |
| 2091 |
Koha::CirculationRules->set_rules( |
| 2092 |
{ |
| 2093 |
branchcode => undef, |
| 2094 |
categorycode => 'XYZ1', |
| 2095 |
itemtype => $itemtype3->{itemtype}, |
| 2096 |
rules => { hold_fee => 2.50, reservesallowed => 10 } |
| 2097 |
} |
| 2098 |
); |
| 2099 |
Koha::CirculationRules->set_rules( |
| 2100 |
{ |
| 2101 |
branchcode => undef, |
| 2102 |
categorycode => 'XYZ1', |
| 2103 |
itemtype => $itemtype_free->{itemtype}, |
| 2104 |
rules => { hold_fee => 2.50, reservesallowed => 10 } |
| 2105 |
} |
| 2106 |
); |
| 2107 |
|
| 2108 |
$fee = $title_hold->calculate_hold_fee(); |
| 2109 |
is( $fee, 2.50, 'Title-level hold: all items with same fee returns that fee regardless of strategy' ); |
| 2110 |
|
| 2111 |
$schema->storage->txn_rollback; |
| 2112 |
}; |
| 2113 |
|
| 2114 |
subtest 'should_charge() tests' => sub { |
| 2115 |
plan tests => 6; |
| 2116 |
|
| 2117 |
$schema->storage->txn_begin; |
| 2118 |
|
| 2119 |
my $patron = $builder->build_object( { class => 'Koha::Patrons' } ); |
| 2120 |
my $biblio = $builder->build_sample_biblio(); |
| 2121 |
my $item = $builder->build_sample_item( { biblionumber => $biblio->biblionumber } ); |
| 2122 |
|
| 2123 |
my $hold = $builder->build_object( |
| 2124 |
{ |
| 2125 |
class => 'Koha::Holds', |
| 2126 |
value => { |
| 2127 |
borrowernumber => $patron->borrowernumber, |
| 2128 |
biblionumber => $biblio->biblionumber, |
| 2129 |
itemnumber => $item->itemnumber, |
| 2130 |
} |
| 2131 |
} |
| 2132 |
); |
| 2133 |
|
| 2134 |
# Test any_time_is_placed mode |
| 2135 |
t::lib::Mocks::mock_preference( 'HoldFeeMode', 'any_time_is_placed' ); |
| 2136 |
ok( $hold->should_charge('placement'), 'any_time_is_placed charges at placement' ); |
| 2137 |
ok( !$hold->should_charge('collection'), 'any_time_is_placed does not charge at collection' ); |
| 2138 |
|
| 2139 |
# Test any_time_is_collected mode |
| 2140 |
t::lib::Mocks::mock_preference( 'HoldFeeMode', 'any_time_is_collected' ); |
| 2141 |
ok( !$hold->should_charge('placement'), 'any_time_is_collected does not charge at placement' ); |
| 2142 |
ok( $hold->should_charge('collection'), 'any_time_is_collected charges at collection' ); |
| 2143 |
|
| 2144 |
# Test not_always mode (basic case - items available) |
| 2145 |
t::lib::Mocks::mock_preference( 'HoldFeeMode', 'not_always' ); |
| 2146 |
ok( !$hold->should_charge('placement'), 'not_always does not charge at placement when items available' ); |
| 2147 |
ok( !$hold->should_charge('collection'), 'not_always does not charge at collection' ); |
| 2148 |
|
| 2149 |
$schema->storage->txn_rollback; |
| 2150 |
}; |
| 2151 |
|
| 2152 |
subtest 'charge_hold_fee() tests' => sub { |
| 2153 |
plan tests => 2; |
| 2154 |
|
| 2155 |
$schema->storage->txn_begin; |
| 2156 |
|
| 2157 |
my $cat = $builder->build( { source => 'Category', value => { categorycode => 'XYZ1' } } ); |
| 2158 |
|
| 2159 |
# Set up circulation rules for hold fees |
| 2160 |
Koha::CirculationRules->set_rules( |
| 2161 |
{ |
| 2162 |
branchcode => undef, |
| 2163 |
categorycode => 'XYZ1', |
| 2164 |
itemtype => undef, |
| 2165 |
rules => { |
| 2166 |
hold_fee => 2.00, |
| 2167 |
} |
| 2168 |
} |
| 2169 |
); |
| 2170 |
|
| 2171 |
my $patron = $builder->build_object( |
| 2172 |
{ |
| 2173 |
class => 'Koha::Patrons', |
| 2174 |
value => { categorycode => 'XYZ1' } |
| 2175 |
} |
| 2176 |
); |
| 2177 |
t::lib::Mocks::mock_userenv( { patron => $patron, branchcode => $patron->branchcode } ); |
| 2178 |
|
| 2179 |
my $biblio = $builder->build_sample_biblio(); |
| 2180 |
my $item = $builder->build_sample_item( { biblionumber => $biblio->biblionumber } ); |
| 2181 |
|
| 2182 |
my $hold = $builder->build_object( |
| 2183 |
{ |
| 2184 |
class => 'Koha::Holds', |
| 2185 |
value => { |
| 2186 |
borrowernumber => $patron->borrowernumber, |
| 2187 |
biblionumber => $biblio->biblionumber, |
| 2188 |
itemnumber => $item->itemnumber, |
| 2189 |
} |
| 2190 |
} |
| 2191 |
); |
| 2192 |
|
| 2193 |
my $account = $patron->account; |
| 2194 |
my $balance_before = $account->balance; |
| 2195 |
|
| 2196 |
# Charge fee |
| 2197 |
my $account_line = $hold->charge_hold_fee(); |
| 2198 |
is( $account_line->amount, 2.00, 'charge_hold_fee returns account line with correct amount' ); |
| 2199 |
|
| 2200 |
my $balance_after = $account->balance; |
| 2201 |
is( $balance_after, $balance_before + 2.00, 'Patron account charged correctly' ); |
| 2202 |
|
| 2203 |
$schema->storage->txn_rollback; |
| 2204 |
}; |