]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/sql/Pg/100.circ_matrix.sql
Require that the matchpoint is_renewal flag be respected if set
[working/Evergreen.git] / Open-ILS / src / sql / Pg / 100.circ_matrix.sql
1
2 BEGIN;
3 -- NOTE: current config.item_type should get sip2_media_type and magnetic_media columns
4
5 -- New table needed to handle circ modifiers inside the DB.  Will still require
6 -- central admin.  The circ_modifier column on asset.copy will become an fkey to this table.
7 CREATE TABLE config.circ_modifier (
8     code            TEXT    PRIMARY KEY,
9     name            TEXT    UNIQUE NOT NULL,
10     description        TEXT    NOT NULL,
11     sip2_media_type    TEXT    NOT NULL,
12     magnetic_media     BOOL    NOT NULL DEFAULT TRUE,
13     avg_wait_time      INTERVAL
14 );
15
16 /*
17 -- for instance ...
18 INSERT INTO config.circ_modifier VALUES ( 'DVD', 'DVD', 'um ... DVDs', '001', FALSE );
19 INSERT INTO config.circ_modifier VALUES ( 'VIDEO', 'VIDEO', 'Tapes', '001', TRUE );
20 INSERT INTO config.circ_modifier VALUES ( 'BOOK', 'BOOK', 'Dead tree', '001', FALSE );
21 INSERT INTO config.circ_modifier VALUES ( 'CRAZY_ARL-ATH_SETTING', 'R2R_TAPE', 'reel2reel tape', '007', TRUE );
22 */
23
24 -- But, just to get us started, use this
25 /*
26
27 UPDATE asset.copy SET circ_modifier = UPPER(circ_modifier) WHERE circ_modifier IS NOT NULL AND circ_modifier <> '';
28 UPDATE asset.copy SET circ_modifier = NULL WHERE circ_modifier = '';
29
30 INSERT INTO config.circ_modifier (code, name, description, sip2_media_type )
31     SELECT DISTINCT
32             UPPER(circ_modifier),
33             UPPER(circ_modifier),
34             LOWER(circ_modifier),
35             '001'
36       FROM  asset.copy
37       WHERE circ_modifier IS NOT NULL;
38
39 */
40
41 -- add an fkey pointing to the new circ mod table
42 ALTER TABLE asset.copy ADD CONSTRAINT circ_mod_fkey FOREIGN KEY (circ_modifier) REFERENCES config.circ_modifier (code) ON UPDATE CASCADE ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED;
43
44 -- config table to hold the vr_format names
45 CREATE TABLE config.videorecording_format_map (
46     code    TEXT    PRIMARY KEY,
47     value    TEXT    NOT NULL
48 );
49
50 INSERT INTO config.videorecording_format_map VALUES ('a','Beta');
51 INSERT INTO config.videorecording_format_map VALUES ('b','VHS');
52 INSERT INTO config.videorecording_format_map VALUES ('c','U-matic');
53 INSERT INTO config.videorecording_format_map VALUES ('d','EIAJ');
54 INSERT INTO config.videorecording_format_map VALUES ('e','Type C');
55 INSERT INTO config.videorecording_format_map VALUES ('f','Quadruplex');
56 INSERT INTO config.videorecording_format_map VALUES ('g','Laserdisc');
57 INSERT INTO config.videorecording_format_map VALUES ('h','CED');
58 INSERT INTO config.videorecording_format_map VALUES ('i','Betacam');
59 INSERT INTO config.videorecording_format_map VALUES ('j','Betacam SP');
60 INSERT INTO config.videorecording_format_map VALUES ('k','Super-VHS');
61 INSERT INTO config.videorecording_format_map VALUES ('m','M-II');
62 INSERT INTO config.videorecording_format_map VALUES ('o','D-2');
63 INSERT INTO config.videorecording_format_map VALUES ('p','8 mm.');
64 INSERT INTO config.videorecording_format_map VALUES ('q','Hi-8 mm.');
65 INSERT INTO config.videorecording_format_map VALUES ('u','Unknown');
66 INSERT INTO config.videorecording_format_map VALUES ('v','DVD');
67 INSERT INTO config.videorecording_format_map VALUES ('z','Other');
68
69
70
71 /**
72  **  Here we define the tables that make up the circ matrix.  Conceptually, this implements
73  **  the "sparse matrix" that everyone talks about, instead of using traditional rules logic.
74  **  Physically, we cut the matrix up into separate tables (almost 3rd normal form!) that handle
75  **  different portions of the matrix.  This wil simplify creation of the UI (I hope), and help the
76  **  developers focus on specific parts of the matrix.
77  **/
78
79
80 --
81 --                 ****** Which ruleset and tests to use *******
82 --
83 -- * Most specific range for org_unit and grp wins.
84 --
85 -- * circ_modifier match takes precidence over marc_type match, if circ_modifier is set here
86 --
87 -- * marc_type is first checked against the circ_as_type from the copy, then the item type from the marc record
88 --
89 -- * If neither circ_modifier nor marc_type is set (both are NULLABLE) then the entry defines the default
90 --   ruleset and tests for the OU + group (like BOOK in PINES)
91 --
92
93 CREATE TABLE config.circ_matrix_matchpoint (
94     id                   SERIAL    PRIMARY KEY,
95     active               BOOL    NOT NULL DEFAULT TRUE,
96     org_unit             INT        NOT NULL REFERENCES actor.org_unit (id) DEFERRABLE INITIALLY DEFERRED,    -- Set to the top OU for the matchpoint applicability range; we can use org_unit_prox to choose the "best"
97     grp                  INT     NOT NULL REFERENCES permission.grp_tree (id) DEFERRABLE INITIALLY DEFERRED,    -- Set to the top applicable group from the group tree; will need descendents and prox functions for filtering
98     circ_modifier        TEXT    REFERENCES config.circ_modifier (code) DEFERRABLE INITIALLY DEFERRED,
99     marc_type            TEXT    REFERENCES config.item_type_map (code) DEFERRABLE INITIALLY DEFERRED,
100     marc_form            TEXT    REFERENCES config.item_form_map (code) DEFERRABLE INITIALLY DEFERRED,
101     marc_vr_format       TEXT    REFERENCES config.videorecording_format_map (code) DEFERRABLE INITIALLY DEFERRED,
102     copy_circ_lib        INT     REFERENCES actor.org_unit (id) DEFERRABLE INITIALLY DEFERRED,
103     copy_owning_lib      INT     REFERENCES actor.org_unit (id) DEFERRABLE INITIALLY DEFERRED,
104     ref_flag             BOOL,
105     juvenile_flag        BOOL,
106     is_renewal           BOOL,
107     usr_age_lower_bound  INTERVAL,
108     usr_age_upper_bound  INTERVAL,
109     circulate            BOOL    NOT NULL DEFAULT TRUE,    -- Hard "can't circ" flag requiring an override
110     duration_rule        INT     NOT NULL REFERENCES config.rule_circ_duration (id) DEFERRABLE INITIALLY DEFERRED,
111     recurring_fine_rule  INT     NOT NULL REFERENCES config.rule_recurring_fine (id) DEFERRABLE INITIALLY DEFERRED,
112     max_fine_rule        INT     NOT NULL REFERENCES config.rule_max_fine (id) DEFERRABLE INITIALLY DEFERRED,
113     script_test          TEXT,                           -- javascript source 
114     total_copy_hold_ratio     FLOAT,
115     available_copy_hold_ratio FLOAT,
116     CONSTRAINT ep_once_per_grp_loc_mod_marc UNIQUE (
117         grp, org_unit, circ_modifier, marc_type, marc_form, marc_vr_format, ref_flag,
118         juvenile_flag, usr_age_lower_bound, usr_age_upper_bound, is_renewal, copy_circ_lib,
119         copy_owning_lib
120     )
121 );
122
123
124 -- Tests for max items out by circ_modifier
125 CREATE TABLE config.circ_matrix_circ_mod_test (
126     id          SERIAL     PRIMARY KEY,
127     matchpoint  INT     NOT NULL REFERENCES config.circ_matrix_matchpoint (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
128     items_out   INT     NOT NULL -- Total current active circulations must be less than this, NULL means skip (always pass)
129 );
130
131 CREATE TABLE config.circ_matrix_circ_mod_test_map (
132     id      SERIAL  PRIMARY KEY,
133     circ_mod_test   INT NOT NULL REFERENCES config.circ_matrix_circ_mod_test (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
134     circ_mod        TEXT    NOT NULL REFERENCES config.circ_modifier (code) ON DELETE CASCADE ON UPDATE CASCADE  DEFERRABLE INITIALLY DEFERRED,
135     CONSTRAINT cm_once_per_test UNIQUE (circ_mod_test, circ_mod)
136 );
137
138 CREATE OR REPLACE FUNCTION action.find_circ_matrix_matchpoint( context_ou INT, match_item BIGINT, match_user INT, renewal BOOL ) RETURNS config.circ_matrix_matchpoint AS $func$
139 DECLARE
140     current_group    permission.grp_tree%ROWTYPE;
141     user_object    actor.usr%ROWTYPE;
142     item_object    asset.copy%ROWTYPE;
143     cn_object    asset.call_number%ROWTYPE;
144     rec_descriptor    metabib.rec_descriptor%ROWTYPE;
145     current_mp    config.circ_matrix_matchpoint%ROWTYPE;
146     matchpoint    config.circ_matrix_matchpoint%ROWTYPE;
147 BEGIN
148     SELECT INTO user_object * FROM actor.usr WHERE id = match_user;
149     SELECT INTO item_object * FROM asset.copy WHERE id = match_item;
150     SELECT INTO cn_object * FROM asset.call_number WHERE id = item_object.call_number;
151     SELECT INTO rec_descriptor r.* FROM metabib.rec_descriptor r JOIN asset.call_number c USING (record) WHERE c.id = item_object.call_number;
152     SELECT INTO current_group * FROM permission.grp_tree WHERE id = user_object.profile;
153
154     LOOP 
155         -- for each potential matchpoint for this ou and group ...
156         FOR current_mp IN
157             SELECT  m.*
158               FROM  config.circ_matrix_matchpoint m
159                     JOIN actor.org_unit_ancestors( context_ou ) d ON (m.org_unit = d.id)
160                     LEFT JOIN actor.org_unit_proximity p ON (p.from_org = context_ou AND p.to_org = d.id)
161               WHERE m.grp = current_group.id
162                     AND m.active
163                     AND (m.copy_owning_lib IS NULL OR cn_object.owning_lib IN ( SELECT id FROM actor.org_unit_descendants(m.copy_owning_lib) ))
164                     AND (m.copy_circ_lib   IS NULL OR item_object.circ_lib IN ( SELECT id FROM actor.org_unit_descendants(m.copy_circ_lib)   ))
165               ORDER BY    CASE WHEN p.prox        IS NULL THEN 999 ELSE p.prox END,
166                     CASE WHEN m.copy_owning_lib IS NOT NULL
167                         THEN 256 / ( SELECT COALESCE(prox, 255) + 1 FROM actor.org_unit_proximity WHERE to_org = cn_object.owning_lib AND from_org = m.copy_owning_lib LIMIT 1 )
168                         ELSE 0
169                     END +
170                     CASE WHEN m.copy_circ_lib IS NOT NULL
171                         THEN 256 / ( SELECT COALESCE(prox, 255) + 1 FROM actor.org_unit_proximity WHERE to_org = item_object.circ_lib AND from_org = m.copy_circ_lib LIMIT 1 )
172                         ELSE 0
173                     END +
174                     CASE WHEN m.is_renewal = renewal        THEN 128 ELSE 0 END +
175                     CASE WHEN m.juvenile_flag    IS NOT NULL THEN 64 ELSE 0 END +
176                     CASE WHEN m.circ_modifier    IS NOT NULL THEN 32 ELSE 0 END +
177                     CASE WHEN m.marc_type        IS NOT NULL THEN 16 ELSE 0 END +
178                     CASE WHEN m.marc_form        IS NOT NULL THEN 8 ELSE 0 END +
179                     CASE WHEN m.marc_vr_format    IS NOT NULL THEN 4 ELSE 0 END +
180                     CASE WHEN m.ref_flag        IS NOT NULL THEN 2 ELSE 0 END +
181                     CASE WHEN m.usr_age_lower_bound    IS NOT NULL THEN 0.5 ELSE 0 END +
182                     CASE WHEN m.usr_age_upper_bound    IS NOT NULL THEN 0.5 ELSE 0 END DESC LOOP
183
184             IF current_mp.is_renewal IS NOT NULL THEN
185                 CONTINUE WHEN current_mp.is_renewal <> renewal;
186             END IF;
187
188             IF current_mp.circ_modifier IS NOT NULL THEN
189                 CONTINUE WHEN current_mp.circ_modifier <> item_object.circ_modifier OR item_object.circ_modifier IS NULL;
190             END IF;
191
192             IF current_mp.marc_type IS NOT NULL THEN
193                 IF item_object.circ_as_type IS NOT NULL THEN
194                     CONTINUE WHEN current_mp.marc_type <> item_object.circ_as_type;
195                 ELSE
196                     CONTINUE WHEN current_mp.marc_type <> rec_descriptor.item_type;
197                 END IF;
198             END IF;
199
200             IF current_mp.marc_form IS NOT NULL THEN
201                 CONTINUE WHEN current_mp.marc_form <> rec_descriptor.item_form;
202             END IF;
203
204             IF current_mp.marc_vr_format IS NOT NULL THEN
205                 CONTINUE WHEN current_mp.marc_vr_format <> rec_descriptor.vr_format;
206             END IF;
207
208             IF current_mp.ref_flag IS NOT NULL THEN
209                 CONTINUE WHEN current_mp.ref_flag <> item_object.ref;
210             END IF;
211
212             IF current_mp.juvenile_flag IS NOT NULL THEN
213                 CONTINUE WHEN current_mp.juvenile_flag <> user_object.juvenile;
214             END IF;
215
216             IF current_mp.usr_age_lower_bound IS NOT NULL THEN
217                 CONTINUE WHEN user_object.dob IS NULL OR current_mp.usr_age_lower_bound < age(user_object.dob);
218             END IF;
219
220             IF current_mp.usr_age_upper_bound IS NOT NULL THEN
221                 CONTINUE WHEN user_object.dob IS NULL OR current_mp.usr_age_upper_bound > age(user_object.dob);
222             END IF;
223
224
225             -- everything was undefined or matched
226             matchpoint = current_mp;
227
228             EXIT WHEN matchpoint.id IS NOT NULL;
229         END LOOP;
230
231         EXIT WHEN current_group.parent IS NULL OR matchpoint.id IS NOT NULL;
232
233         SELECT INTO current_group * FROM permission.grp_tree WHERE id = current_group.parent;
234     END LOOP;
235
236     RETURN matchpoint;
237 END;
238 $func$ LANGUAGE plpgsql;
239
240 CREATE TYPE action.hold_stats AS (
241     hold_count              INT,
242     copy_count              INT,
243     available_count         INT,
244     total_copy_ratio        FLOAT,
245     available_copy_ratio    FLOAT
246 );
247
248 CREATE OR REPLACE FUNCTION action.copy_related_hold_stats (copy_id INT) RETURNS action.hold_stats AS $func$
249 DECLARE
250     output          action.hold_stats%ROWTYPE;
251     hold_count      INT := 0;
252     copy_count      INT := 0;
253     available_count INT := 0;
254     hold_map_data   RECORD;
255 BEGIN
256
257     output.hold_count := 0;
258     output.copy_count := 0;
259     output.available_count := 0;
260
261     SELECT  COUNT( DISTINCT m.hold ) INTO hold_count
262       FROM  action.hold_copy_map m
263             JOIN action.hold_request h ON (m.hold = h.id)
264       WHERE m.target_copy = copy_id
265             AND NOT h.frozen;
266
267     output.hold_count := hold_count;
268
269     IF output.hold_count > 0 THEN
270         FOR hold_map_data IN
271             SELECT  DISTINCT m.target_copy,
272                     acp.status
273               FROM  action.hold_copy_map m
274                     JOIN asset.copy acp ON (m.target_copy = acp.id)
275                     JOIN action.hold_request h ON (m.hold = h.id)
276               WHERE m.hold IN ( SELECT DISTINCT hold FROM action.hold_copy_map WHERE target_copy = copy_id ) AND NOT h.frozen
277         LOOP
278             output.copy_count := output.copy_count + 1;
279             IF hold_map_data.status IN (0,7,12) THEN
280                 output.available_count := output.available_count + 1;
281             END IF;
282         END LOOP;
283         output.total_copy_ratio = output.copy_count::FLOAT / output.hold_count::FLOAT;
284         output.available_copy_ratio = output.available_count::FLOAT / output.hold_count::FLOAT;
285
286     END IF;
287
288     RETURN output;
289
290 END;
291 $func$ LANGUAGE PLPGSQL;
292
293 CREATE TYPE action.matrix_test_result AS ( success BOOL, matchpoint INT, fail_part TEXT );
294 CREATE OR REPLACE FUNCTION action.item_user_circ_test( circ_ou INT, match_item BIGINT, match_user INT, renewal BOOL ) RETURNS SETOF action.matrix_test_result AS $func$
295 DECLARE
296     user_object        actor.usr%ROWTYPE;
297     standing_penalty    config.standing_penalty%ROWTYPE;
298     item_object        asset.copy%ROWTYPE;
299     item_status_object    config.copy_status%ROWTYPE;
300     item_location_object    asset.copy_location%ROWTYPE;
301     result            action.matrix_test_result;
302     circ_test        config.circ_matrix_matchpoint%ROWTYPE;
303     out_by_circ_mod        config.circ_matrix_circ_mod_test%ROWTYPE;
304     circ_mod_map        config.circ_matrix_circ_mod_test_map%ROWTYPE;
305     hold_ratio          action.hold_stats%ROWTYPE;
306     penalty_type         TEXT;
307     tmp_grp         INT;
308     items_out        INT;
309     context_org_list        INT[];
310     done            BOOL := FALSE;
311 BEGIN
312     result.success := TRUE;
313
314     -- Fail if the user is BARRED
315     SELECT INTO user_object * FROM actor.usr WHERE id = match_user;
316
317     -- Fail if we couldn't find the user 
318     IF user_object.id IS NULL THEN
319         result.fail_part := 'no_user';
320         result.success := FALSE;
321         done := TRUE;
322         RETURN NEXT result;
323         RETURN;
324     END IF;
325
326     SELECT INTO item_object * FROM asset.copy WHERE id = match_item;
327
328     -- Fail if we couldn't find the item 
329     IF item_object.id IS NULL THEN
330         result.fail_part := 'no_item';
331         result.success := FALSE;
332         done := TRUE;
333         RETURN NEXT result;
334         RETURN;
335     END IF;
336
337     SELECT INTO circ_test * FROM action.find_circ_matrix_matchpoint(circ_ou, match_item, match_user, renewal);
338     result.matchpoint := circ_test.id;
339
340     -- Fail if we couldn't find a matchpoint
341     IF result.matchpoint IS NULL THEN
342         result.fail_part := 'no_matchpoint';
343         result.success := FALSE;
344         done := TRUE;
345         RETURN NEXT result;
346     END IF;
347
348     IF user_object.barred IS TRUE THEN
349         result.fail_part := 'actor.usr.barred';
350         result.success := FALSE;
351         done := TRUE;
352         RETURN NEXT result;
353     END IF;
354
355     -- Fail if the item can't circulate
356     IF item_object.circulate IS FALSE THEN
357         result.fail_part := 'asset.copy.circulate';
358         result.success := FALSE;
359         done := TRUE;
360         RETURN NEXT result;
361     END IF;
362
363     -- Fail if the item isn't in a circulateable status on a non-renewal
364     IF NOT renewal AND item_object.status NOT IN ( 0, 7, 8 ) THEN 
365         result.fail_part := 'asset.copy.status';
366         result.success := FALSE;
367         done := TRUE;
368         RETURN NEXT result;
369     ELSIF renewal AND item_object.status <> 1 THEN
370         result.fail_part := 'asset.copy.status';
371         result.success := FALSE;
372         done := TRUE;
373         RETURN NEXT result;
374     END IF;
375
376     -- Fail if the item can't circulate because of the shelving location
377     SELECT INTO item_location_object * FROM asset.copy_location WHERE id = item_object.location;
378     IF item_location_object.circulate IS FALSE THEN
379         result.fail_part := 'asset.copy_location.circulate';
380         result.success := FALSE;
381         done := TRUE;
382         RETURN NEXT result;
383     END IF;
384
385     SELECT INTO context_org_list ARRAY_ACCUM(id) FROM actor.org_unit_full_path( circ_test.org_unit );
386
387     -- Fail if the test is set to hard non-circulating
388     IF circ_test.circulate IS FALSE THEN
389         result.fail_part := 'config.circ_matrix_test.circulate';
390         result.success := FALSE;
391         done := TRUE;
392         RETURN NEXT result;
393     END IF;
394
395     -- Fail if the total copy-hold ratio is too low
396     IF circ_test.total_copy_hold_ratio IS NOT NULL THEN
397         SELECT INTO hold_ratio * FROM action.copy_related_hold_stats(match_item);
398         IF hold_ratio.total_copy_ratio IS NOT NULL AND hold_ratio.total_copy_ratio < circ_test.total_copy_hold_ratio THEN
399             result.fail_part := 'config.circ_matrix_test.total_copy_hold_ratio';
400             result.success := FALSE;
401             done := TRUE;
402             RETURN NEXT result;
403         END IF;
404     END IF;
405
406     -- Fail if the available copy-hold ratio is too low
407     IF circ_test.available_copy_hold_ratio IS NOT NULL THEN
408         SELECT INTO hold_ratio * FROM action.copy_related_hold_stats(match_item);
409         IF hold_ratio.available_copy_ratio IS NOT NULL AND hold_ratio.available_copy_ratio < circ_test.available_copy_hold_ratio THEN
410             result.fail_part := 'config.circ_matrix_test.available_copy_hold_ratio';
411             result.success := FALSE;
412             done := TRUE;
413             RETURN NEXT result;
414         END IF;
415     END IF;
416
417     IF renewal THEN
418         penalty_type = '%RENEW%';
419     ELSE
420         penalty_type = '%CIRC%';
421     END IF;
422
423     FOR standing_penalty IN
424         SELECT  DISTINCT csp.*
425           FROM  actor.usr_standing_penalty usp
426                 JOIN config.standing_penalty csp ON (csp.id = usp.standing_penalty)
427           WHERE usr = match_user
428                 AND usp.org_unit IN ( SELECT * FROM explode_array(context_org_list) )
429                 AND (usp.stop_date IS NULL or usp.stop_date > NOW())
430                 AND csp.block_list LIKE penalty_type LOOP
431
432         result.fail_part := standing_penalty.name;
433         result.success := FALSE;
434         done := TRUE;
435         RETURN NEXT result;
436     END LOOP;
437
438     -- Fail if the user has too many items with specific circ_modifiers checked out
439     FOR out_by_circ_mod IN SELECT * FROM config.circ_matrix_circ_mod_test WHERE matchpoint = circ_test.id LOOP
440         SELECT  INTO items_out COUNT(*)
441           FROM  action.circulation circ
442             JOIN asset.copy cp ON (cp.id = circ.target_copy)
443           WHERE circ.usr = match_user
444                AND circ.circ_lib IN ( SELECT * FROM explode_array(context_org_list) )
445             AND circ.checkin_time IS NULL
446             AND (circ.stop_fines IN ('MAXFINES','LONGOVERDUE') OR circ.stop_fines IS NULL)
447             AND cp.circ_modifier IN (SELECT circ_mod FROM config.circ_matrix_circ_mod_test_map WHERE circ_mod_test = out_by_circ_mod.id);
448         IF items_out >= out_by_circ_mod.items_out THEN
449             result.fail_part := 'config.circ_matrix_circ_mod_test';
450             result.success := FALSE;
451             done := TRUE;
452             RETURN NEXT result;
453         END IF;
454     END LOOP;
455
456     -- If we passed everything, return the successful matchpoint id
457     IF NOT done THEN
458         RETURN NEXT result;
459     END IF;
460
461     RETURN;
462 END;
463 $func$ LANGUAGE plpgsql;
464
465 CREATE OR REPLACE FUNCTION action.item_user_circ_test( INT, BIGINT, INT ) RETURNS SETOF action.matrix_test_result AS $func$
466     SELECT * FROM action.item_user_circ_test( $1, $2, $3, FALSE );
467 $func$ LANGUAGE SQL;
468
469 CREATE OR REPLACE FUNCTION action.item_user_renew_test( INT, BIGINT, INT ) RETURNS SETOF action.matrix_test_result AS $func$
470     SELECT * FROM action.item_user_circ_test( $1, $2, $3, TRUE );
471 $func$ LANGUAGE SQL;
472
473
474 CREATE OR REPLACE FUNCTION actor.calculate_system_penalties( match_user INT, context_org INT ) RETURNS SETOF actor.usr_standing_penalty AS $func$
475 DECLARE
476     user_object         actor.usr%ROWTYPE;
477     new_sp_row          actor.usr_standing_penalty%ROWTYPE;
478     existing_sp_row     actor.usr_standing_penalty%ROWTYPE;
479     collections_fines   permission.grp_penalty_threshold%ROWTYPE;
480     max_fines           permission.grp_penalty_threshold%ROWTYPE;
481     max_overdue         permission.grp_penalty_threshold%ROWTYPE;
482     max_items_out       permission.grp_penalty_threshold%ROWTYPE;
483     tmp_grp             INT;
484     items_overdue       INT;
485     items_out           INT;
486     context_org_list    INT[];
487     current_fines        NUMERIC(8,2) := 0.0;
488     tmp_fines            NUMERIC(8,2);
489     tmp_groc            RECORD;
490     tmp_circ            RECORD;
491     tmp_org             actor.org_unit%ROWTYPE;
492     tmp_penalty         config.standing_penalty%ROWTYPE;
493     tmp_depth           INTEGER;
494 BEGIN
495     SELECT INTO user_object * FROM actor.usr WHERE id = match_user;
496
497     -- Max fines
498     SELECT INTO tmp_org * FROM actor.org_unit WHERE id = context_org;
499
500     -- Fail if the user has a high fine balance
501     LOOP
502         tmp_grp := user_object.profile;
503         LOOP
504             SELECT * INTO max_fines FROM permission.grp_penalty_threshold WHERE grp = tmp_grp AND penalty = 1 AND org_unit = tmp_org.id;
505
506             IF max_fines.threshold IS NULL THEN
507                 SELECT parent INTO tmp_grp FROM permission.grp_tree WHERE id = tmp_grp;
508             ELSE
509                 EXIT;
510             END IF;
511
512             IF tmp_grp IS NULL THEN
513                 EXIT;
514             END IF;
515         END LOOP;
516
517         IF max_fines.threshold IS NOT NULL OR tmp_org.parent_ou IS NULL THEN
518             EXIT;
519         END IF;
520
521         SELECT * INTO tmp_org FROM actor.org_unit WHERE id = tmp_org.parent_ou;
522
523     END LOOP;
524
525     IF max_fines.threshold IS NOT NULL THEN
526
527         FOR existing_sp_row IN
528                 SELECT  *
529                   FROM  actor.usr_standing_penalty
530                   WHERE usr = match_user
531                         AND org_unit = max_fines.org_unit
532                         AND (stop_date IS NULL or stop_date > NOW())
533                         AND standing_penalty = 1
534                 LOOP
535             RETURN NEXT existing_sp_row;
536         END LOOP;
537
538         SELECT  SUM(f.balance_owed) INTO current_fines
539           FROM  money.materialized_billable_xact_summary f
540                 JOIN (
541                     SELECT  r.id
542                       FROM  booking.reservation r
543                             JOIN  actor.org_unit_full_path( max_fines.org_unit ) fp ON (r.pickup_lib = fp.id)
544                       WHERE usr = match_user
545                             AND xact_finish IS NULL
546                                 UNION ALL
547                     SELECT  g.id
548                       FROM  money.grocery g
549                             JOIN  actor.org_unit_full_path( max_fines.org_unit ) fp ON (g.billing_location = fp.id)
550                       WHERE usr = match_user
551                             AND xact_finish IS NULL
552                                 UNION ALL
553                     SELECT  circ.id
554                       FROM  action.circulation circ
555                             JOIN  actor.org_unit_full_path( max_fines.org_unit ) fp ON (circ.circ_lib = fp.id)
556                       WHERE usr = match_user
557                             AND xact_finish IS NULL ) l USING (id);
558
559         IF current_fines >= max_fines.threshold THEN
560             new_sp_row.usr := match_user;
561             new_sp_row.org_unit := max_fines.org_unit;
562             new_sp_row.standing_penalty := 1;
563             RETURN NEXT new_sp_row;
564         END IF;
565     END IF;
566
567     -- Start over for max overdue
568     SELECT INTO tmp_org * FROM actor.org_unit WHERE id = context_org;
569
570     -- Fail if the user has too many overdue items
571     LOOP
572         tmp_grp := user_object.profile;
573         LOOP
574
575             SELECT * INTO max_overdue FROM permission.grp_penalty_threshold WHERE grp = tmp_grp AND penalty = 2 AND org_unit = tmp_org.id;
576
577             IF max_overdue.threshold IS NULL THEN
578                 SELECT parent INTO tmp_grp FROM permission.grp_tree WHERE id = tmp_grp;
579             ELSE
580                 EXIT;
581             END IF;
582
583             IF tmp_grp IS NULL THEN
584                 EXIT;
585             END IF;
586         END LOOP;
587
588         IF max_overdue.threshold IS NOT NULL OR tmp_org.parent_ou IS NULL THEN
589             EXIT;
590         END IF;
591
592         SELECT INTO tmp_org * FROM actor.org_unit WHERE id = tmp_org.parent_ou;
593
594     END LOOP;
595
596     IF max_overdue.threshold IS NOT NULL THEN
597
598         FOR existing_sp_row IN
599                 SELECT  *
600                   FROM  actor.usr_standing_penalty
601                   WHERE usr = match_user
602                         AND org_unit = max_overdue.org_unit
603                         AND (stop_date IS NULL or stop_date > NOW())
604                         AND standing_penalty = 2
605                 LOOP
606             RETURN NEXT existing_sp_row;
607         END LOOP;
608
609         SELECT  INTO items_overdue COUNT(*)
610           FROM  action.circulation circ
611                 JOIN  actor.org_unit_full_path( max_overdue.org_unit ) fp ON (circ.circ_lib = fp.id)
612           WHERE circ.usr = match_user
613             AND circ.checkin_time IS NULL
614             AND circ.due_date < NOW()
615             AND (circ.stop_fines = 'MAXFINES' OR circ.stop_fines IS NULL);
616
617         IF items_overdue >= max_overdue.threshold::INT THEN
618             new_sp_row.usr := match_user;
619             new_sp_row.org_unit := max_overdue.org_unit;
620             new_sp_row.standing_penalty := 2;
621             RETURN NEXT new_sp_row;
622         END IF;
623     END IF;
624
625     -- Start over for max out
626     SELECT INTO tmp_org * FROM actor.org_unit WHERE id = context_org;
627
628     -- Fail if the user has too many checked out items
629     LOOP
630         tmp_grp := user_object.profile;
631         LOOP
632             SELECT * INTO max_items_out FROM permission.grp_penalty_threshold WHERE grp = tmp_grp AND penalty = 3 AND org_unit = tmp_org.id;
633
634             IF max_items_out.threshold IS NULL THEN
635                 SELECT parent INTO tmp_grp FROM permission.grp_tree WHERE id = tmp_grp;
636             ELSE
637                 EXIT;
638             END IF;
639
640             IF tmp_grp IS NULL THEN
641                 EXIT;
642             END IF;
643         END LOOP;
644
645         IF max_items_out.threshold IS NOT NULL OR tmp_org.parent_ou IS NULL THEN
646             EXIT;
647         END IF;
648
649         SELECT INTO tmp_org * FROM actor.org_unit WHERE id = tmp_org.parent_ou;
650
651     END LOOP;
652
653
654     -- Fail if the user has too many items checked out
655     IF max_items_out.threshold IS NOT NULL THEN
656
657         FOR existing_sp_row IN
658                 SELECT  *
659                   FROM  actor.usr_standing_penalty
660                   WHERE usr = match_user
661                         AND org_unit = max_items_out.org_unit
662                         AND (stop_date IS NULL or stop_date > NOW())
663                         AND standing_penalty = 3
664                 LOOP
665             RETURN NEXT existing_sp_row;
666         END LOOP;
667
668         SELECT  INTO items_out COUNT(*)
669           FROM  action.circulation circ
670                 JOIN  actor.org_unit_full_path( max_items_out.org_unit ) fp ON (circ.circ_lib = fp.id)
671           WHERE circ.usr = match_user
672                 AND circ.checkin_time IS NULL
673                 AND (circ.stop_fines IN ('MAXFINES','LONGOVERDUE') OR circ.stop_fines IS NULL);
674
675            IF items_out >= max_items_out.threshold::INT THEN
676             new_sp_row.usr := match_user;
677             new_sp_row.org_unit := max_items_out.org_unit;
678             new_sp_row.standing_penalty := 3;
679             RETURN NEXT new_sp_row;
680            END IF;
681     END IF;
682
683     -- Start over for collections warning
684     SELECT INTO tmp_org * FROM actor.org_unit WHERE id = context_org;
685
686     -- Fail if the user has a collections-level fine balance
687     LOOP
688         tmp_grp := user_object.profile;
689         LOOP
690             SELECT * INTO max_fines FROM permission.grp_penalty_threshold WHERE grp = tmp_grp AND penalty = 4 AND org_unit = tmp_org.id;
691
692             IF max_fines.threshold IS NULL THEN
693                 SELECT parent INTO tmp_grp FROM permission.grp_tree WHERE id = tmp_grp;
694             ELSE
695                 EXIT;
696             END IF;
697
698             IF tmp_grp IS NULL THEN
699                 EXIT;
700             END IF;
701         END LOOP;
702
703         IF max_fines.threshold IS NOT NULL OR tmp_org.parent_ou IS NULL THEN
704             EXIT;
705         END IF;
706
707         SELECT * INTO tmp_org FROM actor.org_unit WHERE id = tmp_org.parent_ou;
708
709     END LOOP;
710
711     IF max_fines.threshold IS NOT NULL THEN
712
713         FOR existing_sp_row IN
714                 SELECT  *
715                   FROM  actor.usr_standing_penalty
716                   WHERE usr = match_user
717                         AND org_unit = max_fines.org_unit
718                         AND (stop_date IS NULL or stop_date > NOW())
719                         AND standing_penalty = 4
720                 LOOP
721             RETURN NEXT existing_sp_row;
722         END LOOP;
723
724         SELECT  SUM(f.balance_owed) INTO current_fines
725           FROM  money.materialized_billable_xact_summary f
726                 JOIN (
727                     SELECT  r.id
728                       FROM  booking.reservation r
729                             JOIN  actor.org_unit_full_path( max_fines.org_unit ) fp ON (r.pickup_lib = fp.id)
730                       WHERE usr = match_user
731                             AND xact_finish IS NULL
732                                 UNION ALL
733                     SELECT  g.id
734                       FROM  money.grocery g
735                             JOIN  actor.org_unit_full_path( max_fines.org_unit ) fp ON (g.billing_location = fp.id)
736                       WHERE usr = match_user
737                             AND xact_finish IS NULL
738                                 UNION ALL
739                     SELECT  circ.id
740                       FROM  action.circulation circ
741                             JOIN  actor.org_unit_full_path( max_fines.org_unit ) fp ON (circ.circ_lib = fp.id)
742                       WHERE usr = match_user
743                             AND xact_finish IS NULL ) l USING (id);
744
745         IF current_fines >= max_fines.threshold THEN
746             new_sp_row.usr := match_user;
747             new_sp_row.org_unit := max_fines.org_unit;
748             new_sp_row.standing_penalty := 4;
749             RETURN NEXT new_sp_row;
750         END IF;
751     END IF;
752
753     -- Start over for in collections
754     SELECT INTO tmp_org * FROM actor.org_unit WHERE id = context_org;
755
756     -- Remove the in-collections penalty if the user has paid down enough
757     -- This penalty is different, because this code is not responsible for creating 
758     -- new in-collections penalties, only for removing them
759     LOOP
760         tmp_grp := user_object.profile;
761         LOOP
762             SELECT * INTO max_fines FROM permission.grp_penalty_threshold WHERE grp = tmp_grp AND penalty = 30 AND org_unit = tmp_org.id;
763
764             IF max_fines.threshold IS NULL THEN
765                 SELECT parent INTO tmp_grp FROM permission.grp_tree WHERE id = tmp_grp;
766             ELSE
767                 EXIT;
768             END IF;
769
770             IF tmp_grp IS NULL THEN
771                 EXIT;
772             END IF;
773         END LOOP;
774
775         IF max_fines.threshold IS NOT NULL OR tmp_org.parent_ou IS NULL THEN
776             EXIT;
777         END IF;
778
779         SELECT * INTO tmp_org FROM actor.org_unit WHERE id = tmp_org.parent_ou;
780
781     END LOOP;
782
783     IF max_fines.threshold IS NOT NULL THEN
784
785         -- first, see if the user had paid down to the threshold
786         SELECT  SUM(f.balance_owed) INTO current_fines
787           FROM  money.materialized_billable_xact_summary f
788                 JOIN (
789                     SELECT  r.id
790                       FROM  booking.reservation r
791                             JOIN  actor.org_unit_full_path( max_fines.org_unit ) fp ON (r.pickup_lib = fp.id)
792                       WHERE usr = match_user
793                             AND xact_finish IS NULL
794                                 UNION ALL
795                     SELECT  g.id
796                       FROM  money.grocery g
797                             JOIN  actor.org_unit_full_path( max_fines.org_unit ) fp ON (g.billing_location = fp.id)
798                       WHERE usr = match_user
799                             AND xact_finish IS NULL
800                                 UNION ALL
801                     SELECT  circ.id
802                       FROM  action.circulation circ
803                             JOIN  actor.org_unit_full_path( max_fines.org_unit ) fp ON (circ.circ_lib = fp.id)
804                       WHERE usr = match_user
805                             AND xact_finish IS NULL ) l USING (id);
806
807         IF current_fines IS NULL OR current_fines <= max_fines.threshold THEN
808             -- patron has paid down enough
809
810             SELECT INTO tmp_penalty * FROM config.standing_penalty WHERE id = 30;
811
812             IF tmp_penalty.org_depth IS NOT NULL THEN
813
814                 -- since this code is not responsible for applying the penalty, it can't 
815                 -- guarantee the current context org will match the org at which the penalty 
816                 --- was applied.  search up the org tree until we hit the configured penalty depth
817                 SELECT INTO tmp_org * FROM actor.org_unit WHERE id = context_org;
818                 SELECT INTO tmp_depth depth FROM actor.org_unit_type WHERE id = tmp_org.ou_type;
819
820                 WHILE tmp_depth >= tmp_penalty.org_depth LOOP
821
822                     FOR existing_sp_row IN
823                             SELECT  *
824                             FROM  actor.usr_standing_penalty
825                             WHERE usr = match_user
826                                     AND org_unit = tmp_org.id
827                                     AND (stop_date IS NULL or stop_date > NOW())
828                                     AND standing_penalty = 30 
829                             LOOP
830
831                         -- Penalty exists, return it for removal
832                         RETURN NEXT existing_sp_row;
833                     END LOOP;
834
835                     IF tmp_org.parent_ou IS NULL THEN
836                         EXIT;
837                     END IF;
838
839                     SELECT * INTO tmp_org FROM actor.org_unit WHERE id = tmp_org.parent_ou;
840                     SELECT INTO tmp_depth depth FROM actor.org_unit_type WHERE id = tmp_org.ou_type;
841                 END LOOP;
842
843             ELSE
844
845                 -- no penalty depth is defined, look for exact matches
846
847                 FOR existing_sp_row IN
848                         SELECT  *
849                         FROM  actor.usr_standing_penalty
850                         WHERE usr = match_user
851                                 AND org_unit = max_fines.org_unit
852                                 AND (stop_date IS NULL or stop_date > NOW())
853                                 AND standing_penalty = 30 
854                         LOOP
855                     -- Penalty exists, return it for removal
856                     RETURN NEXT existing_sp_row;
857                 END LOOP;
858             END IF;
859     
860         END IF;
861
862     END IF;
863
864     RETURN;
865 END;
866 $func$ LANGUAGE plpgsql;
867
868 COMMIT;
869