]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/sql/Pg/upgrade/XXXX.schema.copy_loc_circ_limits.sql
Add Copy Location to circ matrix matchpoint
[working/Evergreen.git] / Open-ILS / src / sql / Pg / upgrade / XXXX.schema.copy_loc_circ_limits.sql
1
2 ALTER TABLE config.circ_matrix_weights 
3     ADD COLUMN copy_location NUMERIC(6,2) NOT NULL DEFAULT 5.0;
4 UPDATE config.circ_matrix_weights 
5     SET copy_location = 0.0 WHERE name = 'All_Equal';
6 ALTER TABLE config.circ_matrix_weights 
7     ALTER COLUMN copy_location DROP DEFAULT; -- for consistency w/ baseline schema
8
9 ALTER TABLE config.circ_matrix_matchpoint
10     ADD COLUMN copy_location INTEGER REFERENCES asset.copy_location (id) DEFERRABLE INITIALLY DEFERRED;
11
12 DROP INDEX config.ccmm_once_per_paramset;
13
14 CREATE UNIQUE INDEX ccmm_once_per_paramset ON config.circ_matrix_matchpoint (org_unit, grp, COALESCE(circ_modifier, ''), COALESCE(copy_location::TEXT, ''), COALESCE(marc_type, ''), COALESCE(marc_form, ''), COALESCE(marc_bib_level,''), COALESCE(marc_vr_format, ''), COALESCE(copy_circ_lib::TEXT, ''), COALESCE(copy_owning_lib::TEXT, ''), COALESCE(user_home_ou::TEXT, ''), COALESCE(ref_flag::TEXT, ''), COALESCE(juvenile_flag::TEXT, ''), COALESCE(is_renewal::TEXT, ''), COALESCE(usr_age_lower_bound::TEXT, ''), COALESCE(usr_age_upper_bound::TEXT, ''), COALESCE(item_age::TEXT, '')) WHERE active;
15
16 -- Linkage between limit sets and circ mods
17 CREATE TABLE config.circ_limit_set_copy_loc_map (
18     id          SERIAL  PRIMARY KEY,
19     limit_set   INT     NOT NULL REFERENCES config.circ_limit_set (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
20     copy_loc    INT     NOT NULL REFERENCES asset.copy_location (id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED,
21     CONSTRAINT cl_once_per_set UNIQUE (limit_set, copy_loc)
22 );
23
24 -- Add support for checking config.circ_limit_set_copy_loc_map's
25 CREATE OR REPLACE FUNCTION action.item_user_circ_test( circ_ou INT, match_item BIGINT, match_user INT, renewal BOOL ) 
26     RETURNS SETOF action.circ_matrix_test_result AS $func$
27 DECLARE
28     user_object             actor.usr%ROWTYPE;
29     standing_penalty        config.standing_penalty%ROWTYPE;
30     item_object             asset.copy%ROWTYPE;
31     item_status_object      config.copy_status%ROWTYPE;
32     item_location_object    asset.copy_location%ROWTYPE;
33     result                  action.circ_matrix_test_result;
34     circ_test               action.found_circ_matrix_matchpoint;
35     circ_matchpoint         config.circ_matrix_matchpoint%ROWTYPE;
36     circ_limit_set          config.circ_limit_set%ROWTYPE;
37     hold_ratio              action.hold_stats%ROWTYPE;
38     penalty_type            TEXT;
39     items_out               INT;
40     context_org_list        INT[];
41     done                    BOOL := FALSE;
42 BEGIN
43     -- Assume success unless we hit a failure condition
44     result.success := TRUE;
45
46     -- Need user info to look up matchpoints
47     SELECT INTO user_object * FROM actor.usr WHERE id = match_user AND NOT deleted;
48
49     -- (Insta)Fail if we couldn't find the user
50     IF user_object.id IS NULL THEN
51         result.fail_part := 'no_user';
52         result.success := FALSE;
53         done := TRUE;
54         RETURN NEXT result;
55         RETURN;
56     END IF;
57
58     -- Need item info to look up matchpoints
59     SELECT INTO item_object * FROM asset.copy WHERE id = match_item AND NOT deleted;
60
61     -- (Insta)Fail if we couldn't find the item 
62     IF item_object.id IS NULL THEN
63         result.fail_part := 'no_item';
64         result.success := FALSE;
65         done := TRUE;
66         RETURN NEXT result;
67         RETURN;
68     END IF;
69
70     SELECT INTO circ_test * FROM action.find_circ_matrix_matchpoint(circ_ou, item_object, user_object, renewal);
71
72     circ_matchpoint             := circ_test.matchpoint;
73     result.matchpoint           := circ_matchpoint.id;
74     result.circulate            := circ_matchpoint.circulate;
75     result.duration_rule        := circ_matchpoint.duration_rule;
76     result.recurring_fine_rule  := circ_matchpoint.recurring_fine_rule;
77     result.max_fine_rule        := circ_matchpoint.max_fine_rule;
78     result.hard_due_date        := circ_matchpoint.hard_due_date;
79     result.renewals             := circ_matchpoint.renewals;
80     result.grace_period         := circ_matchpoint.grace_period;
81     result.buildrows            := circ_test.buildrows;
82
83     -- (Insta)Fail if we couldn't find a matchpoint
84     IF circ_test.success = false THEN
85         result.fail_part := 'no_matchpoint';
86         result.success := FALSE;
87         done := TRUE;
88         RETURN NEXT result;
89         RETURN;
90     END IF;
91
92     -- All failures before this point are non-recoverable
93     -- Below this point are possibly overridable failures
94
95     -- Fail if the user is barred
96     IF user_object.barred IS TRUE THEN
97         result.fail_part := 'actor.usr.barred';
98         result.success := FALSE;
99         done := TRUE;
100         RETURN NEXT result;
101     END IF;
102
103     -- Fail if the item can't circulate
104     IF item_object.circulate IS FALSE THEN
105         result.fail_part := 'asset.copy.circulate';
106         result.success := FALSE;
107         done := TRUE;
108         RETURN NEXT result;
109     END IF;
110
111     -- Fail if the item isn't in a circulateable status on a non-renewal
112     IF NOT renewal AND item_object.status NOT IN ( 0, 7, 8 ) THEN 
113         result.fail_part := 'asset.copy.status';
114         result.success := FALSE;
115         done := TRUE;
116         RETURN NEXT result;
117     -- Alternately, fail if the item isn't checked out on a renewal
118     ELSIF renewal AND item_object.status <> 1 THEN
119         result.fail_part := 'asset.copy.status';
120         result.success := FALSE;
121         done := TRUE;
122         RETURN NEXT result;
123     END IF;
124
125     -- Fail if the item can't circulate because of the shelving location
126     SELECT INTO item_location_object * FROM asset.copy_location WHERE id = item_object.location;
127     IF item_location_object.circulate IS FALSE THEN
128         result.fail_part := 'asset.copy_location.circulate';
129         result.success := FALSE;
130         done := TRUE;
131         RETURN NEXT result;
132     END IF;
133
134     -- Use Circ OU for penalties and such
135     SELECT INTO context_org_list ARRAY_AGG(id) FROM actor.org_unit_full_path( circ_ou );
136
137     IF renewal THEN
138         penalty_type = '%RENEW%';
139     ELSE
140         penalty_type = '%CIRC%';
141     END IF;
142
143     FOR standing_penalty IN
144         SELECT  DISTINCT csp.*
145           FROM  actor.usr_standing_penalty usp
146                 JOIN config.standing_penalty csp ON (csp.id = usp.standing_penalty)
147           WHERE usr = match_user
148                 AND usp.org_unit IN ( SELECT * FROM unnest(context_org_list) )
149                 AND (usp.stop_date IS NULL or usp.stop_date > NOW())
150                 AND csp.block_list LIKE penalty_type LOOP
151
152         result.fail_part := standing_penalty.name;
153         result.success := FALSE;
154         done := TRUE;
155         RETURN NEXT result;
156     END LOOP;
157
158     -- Fail if the test is set to hard non-circulating
159     IF circ_matchpoint.circulate IS FALSE THEN
160         result.fail_part := 'config.circ_matrix_test.circulate';
161         result.success := FALSE;
162         done := TRUE;
163         RETURN NEXT result;
164     END IF;
165
166     -- Fail if the total copy-hold ratio is too low
167     IF circ_matchpoint.total_copy_hold_ratio IS NOT NULL THEN
168         SELECT INTO hold_ratio * FROM action.copy_related_hold_stats(match_item);
169         IF hold_ratio.total_copy_ratio IS NOT NULL AND hold_ratio.total_copy_ratio < circ_matchpoint.total_copy_hold_ratio THEN
170             result.fail_part := 'config.circ_matrix_test.total_copy_hold_ratio';
171             result.success := FALSE;
172             done := TRUE;
173             RETURN NEXT result;
174         END IF;
175     END IF;
176
177     -- Fail if the available copy-hold ratio is too low
178     IF circ_matchpoint.available_copy_hold_ratio IS NOT NULL THEN
179         IF hold_ratio.hold_count IS NULL THEN
180             SELECT INTO hold_ratio * FROM action.copy_related_hold_stats(match_item);
181         END IF;
182         IF hold_ratio.available_copy_ratio IS NOT NULL AND hold_ratio.available_copy_ratio < circ_matchpoint.available_copy_hold_ratio THEN
183             result.fail_part := 'config.circ_matrix_test.available_copy_hold_ratio';
184             result.success := FALSE;
185             done := TRUE;
186             RETURN NEXT result;
187         END IF;
188     END IF;
189
190     -- Fail if the user has too many items out by defined limit sets
191     FOR circ_limit_set IN SELECT ccls.* FROM config.circ_limit_set ccls
192       JOIN config.circ_matrix_limit_set_map ccmlsm ON ccmlsm.limit_set = ccls.id
193       WHERE ccmlsm.active AND ( ccmlsm.matchpoint = circ_matchpoint.id OR
194         ( ccmlsm.matchpoint IN (SELECT * FROM unnest(result.buildrows)) AND ccmlsm.fallthrough )
195         ) LOOP
196             IF circ_limit_set.items_out > 0 AND NOT renewal THEN
197                 SELECT INTO context_org_list ARRAY_AGG(aou.id)
198                   FROM actor.org_unit_full_path( circ_ou ) aou
199                     JOIN actor.org_unit_type aout ON aou.ou_type = aout.id
200                   WHERE aout.depth >= circ_limit_set.depth;
201                 IF circ_limit_set.global THEN
202                     WITH RECURSIVE descendant_depth AS (
203                         SELECT  ou.id,
204                             ou.parent_ou
205                         FROM  actor.org_unit ou
206                         WHERE ou.id IN (SELECT * FROM unnest(context_org_list))
207                             UNION
208                         SELECT  ou.id,
209                             ou.parent_ou
210                         FROM  actor.org_unit ou
211                             JOIN descendant_depth ot ON (ot.id = ou.parent_ou)
212                     ) SELECT INTO context_org_list ARRAY_AGG(ou.id) FROM actor.org_unit ou JOIN descendant_depth USING (id);
213                 END IF;
214                 SELECT INTO items_out COUNT(DISTINCT circ.id)
215                   FROM action.circulation circ
216                     JOIN asset.copy copy ON (copy.id = circ.target_copy)
217                     LEFT JOIN action.circulation_limit_group_map aclgm ON (circ.id = aclgm.circ)
218                   WHERE circ.usr = match_user
219                     AND circ.circ_lib IN (SELECT * FROM unnest(context_org_list))
220                     AND circ.checkin_time IS NULL
221                     AND (circ.stop_fines IN ('MAXFINES','LONGOVERDUE') OR circ.stop_fines IS NULL)
222                     AND (copy.circ_modifier IN (SELECT circ_mod FROM config.circ_limit_set_circ_mod_map WHERE limit_set = circ_limit_set.id)
223                         OR copy.location IN (SELECT copy_loc FROM config.circ_limit_set_copy_loc_map WHERE limit_set = circ_limit_set.id)
224                         OR aclgm.limit_group IN (SELECT limit_group FROM config.circ_limit_set_group_map WHERE limit_set = circ_limit_set.id)
225                     );
226                 IF items_out >= circ_limit_set.items_out THEN
227                     result.fail_part := 'config.circ_matrix_circ_mod_test';
228                     result.success := FALSE;
229                     done := TRUE;
230                     RETURN NEXT result;
231                 END IF;
232             END IF;
233             SELECT INTO result.limit_groups result.limit_groups || ARRAY_AGG(limit_group) FROM config.circ_limit_set_group_map WHERE limit_set = circ_limit_set.id AND NOT check_only;
234     END LOOP;
235
236     -- If we passed everything, return the successful matchpoint
237     IF NOT done THEN
238         RETURN NEXT result;
239     END IF;
240
241     RETURN;
242 END;
243 $func$ LANGUAGE plpgsql;
244
245
246 -- adding copy_loc to circ_matrix_matchpoint
247 CREATE OR REPLACE FUNCTION action.find_circ_matrix_matchpoint( context_ou INT, item_object asset.copy, user_object actor.usr, renewal BOOL ) RETURNS action.found_circ_matrix_matchpoint AS $func$
248 DECLARE
249     cn_object       asset.call_number%ROWTYPE;
250     rec_descriptor  metabib.rec_descriptor%ROWTYPE;
251     cur_matchpoint  config.circ_matrix_matchpoint%ROWTYPE;
252     matchpoint      config.circ_matrix_matchpoint%ROWTYPE;
253     weights         config.circ_matrix_weights%ROWTYPE;
254     user_age        INTERVAL;
255     my_item_age     INTERVAL;
256     denominator     NUMERIC(6,2);
257     row_list        INT[];
258     result          action.found_circ_matrix_matchpoint;
259 BEGIN
260     -- Assume failure
261     result.success = false;
262
263     -- Fetch useful data
264     SELECT INTO cn_object       * FROM asset.call_number        WHERE id = item_object.call_number;
265     SELECT INTO rec_descriptor  * FROM metabib.rec_descriptor   WHERE record = cn_object.record;
266
267     -- Pre-generate this so we only calc it once
268     IF user_object.dob IS NOT NULL THEN
269         SELECT INTO user_age age(user_object.dob);
270     END IF;
271
272     -- Ditto
273     SELECT INTO my_item_age age(coalesce(item_object.active_date, now()));
274
275     -- Grab the closest set circ weight setting.
276     SELECT INTO weights cw.*
277       FROM config.weight_assoc wa
278            JOIN config.circ_matrix_weights cw ON (cw.id = wa.circ_weights)
279            JOIN actor.org_unit_ancestors_distance( context_ou ) d ON (wa.org_unit = d.id)
280       WHERE active
281       ORDER BY d.distance
282       LIMIT 1;
283
284     -- No weights? Bad admin! Defaults to handle that anyway.
285     IF weights.id IS NULL THEN
286         weights.grp                 := 11.0;
287         weights.org_unit            := 10.0;
288         weights.circ_modifier       := 5.0;
289         weights.copy_location       := 5.0;
290         weights.marc_type           := 4.0;
291         weights.marc_form           := 3.0;
292         weights.marc_bib_level      := 2.0;
293         weights.marc_vr_format      := 2.0;
294         weights.copy_circ_lib       := 8.0;
295         weights.copy_owning_lib     := 8.0;
296         weights.user_home_ou        := 8.0;
297         weights.ref_flag            := 1.0;
298         weights.juvenile_flag       := 6.0;
299         weights.is_renewal          := 7.0;
300         weights.usr_age_lower_bound := 0.0;
301         weights.usr_age_upper_bound := 0.0;
302         weights.item_age            := 0.0;
303     END IF;
304
305     -- Determine the max (expected) depth (+1) of the org tree and max depth of the permisson tree
306     -- If you break your org tree with funky parenting this may be wrong
307     -- Note: This CTE is duplicated in the find_hold_matrix_matchpoint function, and it may be a good idea to split it off to a function
308     -- We use one denominator for all tree-based checks for when permission groups and org units have the same weighting
309     WITH all_distance(distance) AS (
310             SELECT depth AS distance FROM actor.org_unit_type
311         UNION
312             SELECT distance AS distance FROM permission.grp_ancestors_distance((SELECT id FROM permission.grp_tree WHERE parent IS NULL))
313         )
314     SELECT INTO denominator MAX(distance) + 1 FROM all_distance;
315
316     -- Loop over all the potential matchpoints
317     FOR cur_matchpoint IN
318         SELECT m.*
319           FROM  config.circ_matrix_matchpoint m
320                 /*LEFT*/ JOIN permission.grp_ancestors_distance( user_object.profile ) upgad ON m.grp = upgad.id
321                 /*LEFT*/ JOIN actor.org_unit_ancestors_distance( context_ou ) ctoua ON m.org_unit = ctoua.id
322                 LEFT JOIN actor.org_unit_ancestors_distance( cn_object.owning_lib ) cnoua ON m.copy_owning_lib = cnoua.id
323                 LEFT JOIN actor.org_unit_ancestors_distance( item_object.circ_lib ) iooua ON m.copy_circ_lib = iooua.id
324                 LEFT JOIN actor.org_unit_ancestors_distance( user_object.home_ou  ) uhoua ON m.user_home_ou = uhoua.id
325           WHERE m.active
326                 -- Permission Groups
327              -- AND (m.grp                      IS NULL OR upgad.id IS NOT NULL) -- Optional Permission Group?
328                 -- Org Units
329              -- AND (m.org_unit                 IS NULL OR ctoua.id IS NOT NULL) -- Optional Org Unit?
330                 AND (m.copy_owning_lib          IS NULL OR cnoua.id IS NOT NULL)
331                 AND (m.copy_circ_lib            IS NULL OR iooua.id IS NOT NULL)
332                 AND (m.user_home_ou             IS NULL OR uhoua.id IS NOT NULL)
333                 -- Circ Type
334                 AND (m.is_renewal               IS NULL OR m.is_renewal = renewal)
335                 -- Static User Checks
336                 AND (m.juvenile_flag            IS NULL OR m.juvenile_flag = user_object.juvenile)
337                 AND (m.usr_age_lower_bound      IS NULL OR (user_age IS NOT NULL AND m.usr_age_lower_bound < user_age))
338                 AND (m.usr_age_upper_bound      IS NULL OR (user_age IS NOT NULL AND m.usr_age_upper_bound > user_age))
339                 -- Static Item Checks
340                 AND (m.circ_modifier            IS NULL OR m.circ_modifier = item_object.circ_modifier)
341                 AND (m.copy_location            IS NULL OR m.copy_location = item_object.location)
342                 AND (m.marc_type                IS NULL OR m.marc_type = COALESCE(item_object.circ_as_type, rec_descriptor.item_type))
343                 AND (m.marc_form                IS NULL OR m.marc_form = rec_descriptor.item_form)
344                 AND (m.marc_bib_level           IS NULL OR m.marc_bib_level = rec_descriptor.bib_level)
345                 AND (m.marc_vr_format           IS NULL OR m.marc_vr_format = rec_descriptor.vr_format)
346                 AND (m.ref_flag                 IS NULL OR m.ref_flag = item_object.ref)
347                 AND (m.item_age                 IS NULL OR (my_item_age IS NOT NULL AND m.item_age > my_item_age))
348           ORDER BY
349                 -- Permission Groups
350                 CASE WHEN upgad.distance        IS NOT NULL THEN 2^(2*weights.grp - (upgad.distance/denominator)) ELSE 0.0 END +
351                 -- Org Units
352                 CASE WHEN ctoua.distance        IS NOT NULL THEN 2^(2*weights.org_unit - (ctoua.distance/denominator)) ELSE 0.0 END +
353                 CASE WHEN cnoua.distance        IS NOT NULL THEN 2^(2*weights.copy_owning_lib - (cnoua.distance/denominator)) ELSE 0.0 END +
354                 CASE WHEN iooua.distance        IS NOT NULL THEN 2^(2*weights.copy_circ_lib - (iooua.distance/denominator)) ELSE 0.0 END +
355                 CASE WHEN uhoua.distance        IS NOT NULL THEN 2^(2*weights.user_home_ou - (uhoua.distance/denominator)) ELSE 0.0 END +
356                 -- Circ Type                    -- Note: 4^x is equiv to 2^(2*x)
357                 CASE WHEN m.is_renewal          IS NOT NULL THEN 4^weights.is_renewal ELSE 0.0 END +
358                 -- Static User Checks
359                 CASE WHEN m.juvenile_flag       IS NOT NULL THEN 4^weights.juvenile_flag ELSE 0.0 END +
360                 CASE WHEN m.usr_age_lower_bound IS NOT NULL THEN 4^weights.usr_age_lower_bound ELSE 0.0 END +
361                 CASE WHEN m.usr_age_upper_bound IS NOT NULL THEN 4^weights.usr_age_upper_bound ELSE 0.0 END +
362                 -- Static Item Checks
363                 CASE WHEN m.circ_modifier       IS NOT NULL THEN 4^weights.circ_modifier ELSE 0.0 END +
364                 CASE WHEN m.copy_location       IS NOT NULL THEN 4^weights.copy_location ELSE 0.0 END +
365                 CASE WHEN m.marc_type           IS NOT NULL THEN 4^weights.marc_type ELSE 0.0 END +
366                 CASE WHEN m.marc_form           IS NOT NULL THEN 4^weights.marc_form ELSE 0.0 END +
367                 CASE WHEN m.marc_vr_format      IS NOT NULL THEN 4^weights.marc_vr_format ELSE 0.0 END +
368                 CASE WHEN m.ref_flag            IS NOT NULL THEN 4^weights.ref_flag ELSE 0.0 END +
369                 -- Item age has a slight adjustment to weight based on value.
370                 -- This should ensure that a shorter age limit comes first when all else is equal.
371                 -- NOTE: This assumes that intervals will normally be in days.
372                 CASE WHEN m.item_age            IS NOT NULL THEN 4^weights.item_age - 1 + 86400/EXTRACT(EPOCH FROM m.item_age) ELSE 0.0 END DESC,
373                 -- Final sort on id, so that if two rules have the same sorting in the previous sort they have a defined order
374                 -- This prevents "we changed the table order by updating a rule, and we started getting different results"
375                 m.id LOOP
376
377         -- Record the full matching row list
378         row_list := row_list || cur_matchpoint.id;
379
380         -- No matchpoint yet?
381         IF matchpoint.id IS NULL THEN
382             -- Take the entire matchpoint as a starting point
383             matchpoint := cur_matchpoint;
384             CONTINUE; -- No need to look at this row any more.
385         END IF;
386
387         -- Incomplete matchpoint?
388         IF matchpoint.circulate IS NULL THEN
389             matchpoint.circulate := cur_matchpoint.circulate;
390         END IF;
391         IF matchpoint.duration_rule IS NULL THEN
392             matchpoint.duration_rule := cur_matchpoint.duration_rule;
393         END IF;
394         IF matchpoint.recurring_fine_rule IS NULL THEN
395             matchpoint.recurring_fine_rule := cur_matchpoint.recurring_fine_rule;
396         END IF;
397         IF matchpoint.max_fine_rule IS NULL THEN
398             matchpoint.max_fine_rule := cur_matchpoint.max_fine_rule;
399         END IF;
400         IF matchpoint.hard_due_date IS NULL THEN
401             matchpoint.hard_due_date := cur_matchpoint.hard_due_date;
402         END IF;
403         IF matchpoint.total_copy_hold_ratio IS NULL THEN
404             matchpoint.total_copy_hold_ratio := cur_matchpoint.total_copy_hold_ratio;
405         END IF;
406         IF matchpoint.available_copy_hold_ratio IS NULL THEN
407             matchpoint.available_copy_hold_ratio := cur_matchpoint.available_copy_hold_ratio;
408         END IF;
409         IF matchpoint.renewals IS NULL THEN
410             matchpoint.renewals := cur_matchpoint.renewals;
411         END IF;
412         IF matchpoint.grace_period IS NULL THEN
413             matchpoint.grace_period := cur_matchpoint.grace_period;
414         END IF;
415     END LOOP;
416
417     -- Check required fields
418     IF matchpoint.circulate             IS NOT NULL AND
419        matchpoint.duration_rule         IS NOT NULL AND
420        matchpoint.recurring_fine_rule   IS NOT NULL AND
421        matchpoint.max_fine_rule         IS NOT NULL THEN
422         -- All there? We have a completed match.
423         result.success := true;
424     END IF;
425
426     -- Include the assembled matchpoint, even if it isn't complete
427     result.matchpoint := matchpoint;
428
429     -- Include (for debugging) the full list of matching rows
430     result.buildrows := row_list;
431
432     -- Hand the result back to caller
433     RETURN result;
434 END;
435 $func$ LANGUAGE plpgsql;
436
437