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 |
}; |