LP 1499123: Stamping upgrade script for standing-penalty-ignore-proximity
authorKathy Lussier <klussier@masslnc.org>
Tue, 16 Feb 2016 19:57:07 +0000 (14:57 -0500)
committerKathy Lussier <klussier@masslnc.org>
Tue, 16 Feb 2016 20:01:04 +0000 (15:01 -0500)
Nearly forgot to stamp the upgrade script for lp1499123. While I'm at it,
correcting a small typo found in the release notes.

Signed-off-by: Kathy Lussier <klussier@masslnc.org>
Open-ILS/src/sql/Pg/002.schema.config.sql
Open-ILS/src/sql/Pg/upgrade/0951.schema.config.standing_penalty.ignore_proximity.sql [new file with mode: 0644]
Open-ILS/src/sql/Pg/upgrade/XXXX.schema.config.standing_penalty.ignore_proximity.sql [deleted file]
docs/RELEASE_NOTES_NEXT/Circulation/standing_penalty_ignore_proximity.txt

index f46affe..2191024 100644 (file)
@@ -91,7 +91,7 @@ CREATE TRIGGER no_overlapping_deps
     BEFORE INSERT OR UPDATE ON config.db_patch_dependencies
     FOR EACH ROW EXECUTE PROCEDURE evergreen.array_overlap_check ('deprecates');
 
-INSERT INTO config.upgrade_log (version, applied_to) VALUES ('0950', :eg_version); -- bmagic/kmlussier
+INSERT INTO config.upgrade_log (version, applied_to) VALUES ('0951', :eg_version); -- dyrcona/kmlussier
 
 CREATE TABLE config.bib_source (
        id              SERIAL  PRIMARY KEY,
diff --git a/Open-ILS/src/sql/Pg/upgrade/0951.schema.config.standing_penalty.ignore_proximity.sql b/Open-ILS/src/sql/Pg/upgrade/0951.schema.config.standing_penalty.ignore_proximity.sql
new file mode 100644 (file)
index 0000000..3452869
--- /dev/null
@@ -0,0 +1,478 @@
+BEGIN;
+
+--SELECT evergreen.upgrade_deps_block_check('0951', :eg_version);
+
+ALTER TABLE config.standing_penalty
+      ADD COLUMN ignore_proximity INTEGER;
+
+CREATE OR REPLACE FUNCTION action.hold_request_permit_test( pickup_ou INT, request_ou INT, match_item BIGINT, match_user INT, match_requestor INT, retargetting BOOL ) RETURNS SETOF action.matrix_test_result AS $func$
+DECLARE
+    matchpoint_id        INT;
+    user_object        actor.usr%ROWTYPE;
+    age_protect_object    config.rule_age_hold_protect%ROWTYPE;
+    standing_penalty    config.standing_penalty%ROWTYPE;
+    transit_range_ou_type    actor.org_unit_type%ROWTYPE;
+    transit_source        actor.org_unit%ROWTYPE;
+    item_object        asset.copy%ROWTYPE;
+    item_cn_object     asset.call_number%ROWTYPE;
+    item_status_object  config.copy_status%ROWTYPE;
+    item_location_object    asset.copy_location%ROWTYPE;
+    ou_skip              actor.org_unit_setting%ROWTYPE;
+    result            action.matrix_test_result;
+    hold_test        config.hold_matrix_matchpoint%ROWTYPE;
+    use_active_date   TEXT;
+    age_protect_date  TIMESTAMP WITH TIME ZONE;
+    hold_count        INT;
+    hold_transit_prox    INT;
+    frozen_hold_count    INT;
+    context_org_list    INT[];
+    done            BOOL := FALSE;
+    hold_penalty TEXT;
+    v_pickup_ou ALIAS FOR pickup_ou;
+    v_request_ou ALIAS FOR request_ou;
+    item_prox INT;
+    pickup_prox INT;
+BEGIN
+    SELECT INTO user_object * FROM actor.usr WHERE id = match_user;
+    SELECT INTO context_org_list ARRAY_AGG(id) FROM actor.org_unit_full_path( v_pickup_ou );
+
+    result.success := TRUE;
+
+    -- The HOLD penalty block only applies to new holds.
+    -- The CAPTURE penalty block applies to existing holds.
+    hold_penalty := 'HOLD';
+    IF retargetting THEN
+        hold_penalty := 'CAPTURE';
+    END IF;
+
+    -- Fail if we couldn't find a user
+    IF user_object.id IS NULL THEN
+        result.fail_part := 'no_user';
+        result.success := FALSE;
+        done := TRUE;
+        RETURN NEXT result;
+        RETURN;
+    END IF;
+
+    SELECT INTO item_object * FROM asset.copy WHERE id = match_item;
+
+    -- Fail if we couldn't find a copy
+    IF item_object.id IS NULL THEN
+        result.fail_part := 'no_item';
+        result.success := FALSE;
+        done := TRUE;
+        RETURN NEXT result;
+        RETURN;
+    END IF;
+
+    SELECT INTO matchpoint_id action.find_hold_matrix_matchpoint(v_pickup_ou, v_request_ou, match_item, match_user, match_requestor);
+    result.matchpoint := matchpoint_id;
+
+    SELECT INTO ou_skip * FROM actor.org_unit_setting WHERE name = 'circ.holds.target_skip_me' AND org_unit = item_object.circ_lib;
+
+    -- Fail if the circ_lib for the item has circ.holds.target_skip_me set to true
+    IF ou_skip.id IS NOT NULL AND ou_skip.value = 'true' THEN
+        result.fail_part := 'circ.holds.target_skip_me';
+        result.success := FALSE;
+        done := TRUE;
+        RETURN NEXT result;
+        RETURN;
+    END IF;
+
+    -- Fail if user is barred
+    IF user_object.barred IS TRUE THEN
+        result.fail_part := 'actor.usr.barred';
+        result.success := FALSE;
+        done := TRUE;
+        RETURN NEXT result;
+        RETURN;
+    END IF;
+
+    SELECT INTO item_cn_object * FROM asset.call_number WHERE id = item_object.call_number;
+    SELECT INTO item_status_object * FROM config.copy_status WHERE id = item_object.status;
+    SELECT INTO item_location_object * FROM asset.copy_location WHERE id = item_object.location;
+
+    -- Fail if we couldn't find any matchpoint (requires a default)
+    IF matchpoint_id IS NULL THEN
+        result.fail_part := 'no_matchpoint';
+        result.success := FALSE;
+        done := TRUE;
+        RETURN NEXT result;
+        RETURN;
+    END IF;
+
+    SELECT INTO hold_test * FROM config.hold_matrix_matchpoint WHERE id = matchpoint_id;
+
+    IF hold_test.holdable IS FALSE THEN
+        result.fail_part := 'config.hold_matrix_test.holdable';
+        result.success := FALSE;
+        done := TRUE;
+        RETURN NEXT result;
+    END IF;
+
+    IF item_object.holdable IS FALSE THEN
+        result.fail_part := 'item.holdable';
+        result.success := FALSE;
+        done := TRUE;
+        RETURN NEXT result;
+    END IF;
+
+    IF item_status_object.holdable IS FALSE THEN
+        result.fail_part := 'status.holdable';
+        result.success := FALSE;
+        done := TRUE;
+        RETURN NEXT result;
+    END IF;
+
+    IF item_location_object.holdable IS FALSE THEN
+        result.fail_part := 'location.holdable';
+        result.success := FALSE;
+        done := TRUE;
+        RETURN NEXT result;
+    END IF;
+
+    IF hold_test.transit_range IS NOT NULL THEN
+        SELECT INTO transit_range_ou_type * FROM actor.org_unit_type WHERE id = hold_test.transit_range;
+        IF hold_test.distance_is_from_owner THEN
+            SELECT INTO transit_source ou.* FROM actor.org_unit ou JOIN asset.call_number cn ON (cn.owning_lib = ou.id) WHERE cn.id = item_object.call_number;
+        ELSE
+            SELECT INTO transit_source * FROM actor.org_unit WHERE id = item_object.circ_lib;
+        END IF;
+
+        PERFORM * FROM actor.org_unit_descendants( transit_source.id, transit_range_ou_type.depth ) WHERE id = v_pickup_ou;
+
+        IF NOT FOUND THEN
+            result.fail_part := 'transit_range';
+            result.success := FALSE;
+            done := TRUE;
+            RETURN NEXT result;
+        END IF;
+    END IF;
+    -- Proximity of user's home_ou to the pickup_lib to see if penalty should be ignored.
+    SELECT INTO pickup_prox prox FROM actor.org_unit_proximity WHERE from_org = user_object.home_ou AND to_org = v_pickup_ou;
+    -- Proximity of user's home_ou to the items' lib to see if penalty should be ignored.
+    IF hold_test.distance_is_from_owner THEN
+        SELECT INTO item_prox prox FROM actor.org_unit_proximity WHERE from_org = user_object.home_ou AND to_org = item_cn_object.owning_lib;
+    ELSE
+        SELECT INTO item_prox prox FROM actor.org_unit_proximity WHERE from_org = user_object.home_ou AND to_org = item_object.circ_lib;
+    END IF;
+
+    FOR standing_penalty IN
+        SELECT  DISTINCT csp.*
+          FROM  actor.usr_standing_penalty usp
+                JOIN config.standing_penalty csp ON (csp.id = usp.standing_penalty)
+          WHERE usr = match_user
+                AND usp.org_unit IN ( SELECT * FROM unnest(context_org_list) )
+                AND (usp.stop_date IS NULL or usp.stop_date > NOW())
+                AND (csp.ignore_proximity IS NULL OR csp.ignore_proximity < item_prox
+                     OR csp.ignore_proximity < pickup_prox)
+                AND csp.block_list LIKE '%' || hold_penalty || '%' LOOP
+
+        result.fail_part := standing_penalty.name;
+        result.success := FALSE;
+        done := TRUE;
+        RETURN NEXT result;
+    END LOOP;
+
+    IF hold_test.stop_blocked_user IS TRUE THEN
+        FOR standing_penalty IN
+            SELECT  DISTINCT csp.*
+              FROM  actor.usr_standing_penalty usp
+                    JOIN config.standing_penalty csp ON (csp.id = usp.standing_penalty)
+              WHERE usr = match_user
+                    AND usp.org_unit IN ( SELECT * FROM unnest(context_org_list) )
+                    AND (usp.stop_date IS NULL or usp.stop_date > NOW())
+                    AND csp.block_list LIKE '%CIRC%' LOOP
+    
+            result.fail_part := standing_penalty.name;
+            result.success := FALSE;
+            done := TRUE;
+            RETURN NEXT result;
+        END LOOP;
+    END IF;
+
+    IF hold_test.max_holds IS NOT NULL AND NOT retargetting THEN
+        SELECT    INTO hold_count COUNT(*)
+          FROM    action.hold_request
+          WHERE    usr = match_user
+            AND fulfillment_time IS NULL
+            AND cancel_time IS NULL
+            AND CASE WHEN hold_test.include_frozen_holds THEN TRUE ELSE frozen IS FALSE END;
+
+        IF hold_count >= hold_test.max_holds THEN
+            result.fail_part := 'config.hold_matrix_test.max_holds';
+            result.success := FALSE;
+            done := TRUE;
+            RETURN NEXT result;
+        END IF;
+    END IF;
+
+    IF item_object.age_protect IS NOT NULL THEN
+        SELECT INTO age_protect_object * FROM config.rule_age_hold_protect WHERE id = item_object.age_protect;
+        IF hold_test.distance_is_from_owner THEN
+            SELECT INTO use_active_date value FROM actor.org_unit_ancestor_setting('circ.holds.age_protect.active_date', item_cn_object.owning_lib);
+        ELSE
+            SELECT INTO use_active_date value FROM actor.org_unit_ancestor_setting('circ.holds.age_protect.active_date', item_object.circ_lib);
+        END IF;
+        IF use_active_date = 'true' THEN
+            age_protect_date := COALESCE(item_object.active_date, NOW());
+        ELSE
+            age_protect_date := item_object.create_date;
+        END IF;
+        IF age_protect_date + age_protect_object.age > NOW() THEN
+            IF hold_test.distance_is_from_owner THEN
+                SELECT INTO item_cn_object * FROM asset.call_number WHERE id = item_object.call_number;
+                SELECT INTO hold_transit_prox prox FROM actor.org_unit_proximity WHERE from_org = item_cn_object.owning_lib AND to_org = v_pickup_ou;
+            ELSE
+                SELECT INTO hold_transit_prox prox FROM actor.org_unit_proximity WHERE from_org = item_object.circ_lib AND to_org = v_pickup_ou;
+            END IF;
+
+            IF hold_transit_prox > age_protect_object.prox THEN
+                result.fail_part := 'config.rule_age_hold_protect.prox';
+                result.success := FALSE;
+                done := TRUE;
+                RETURN NEXT result;
+            END IF;
+        END IF;
+    END IF;
+
+    IF NOT done THEN
+        RETURN NEXT result;
+    END IF;
+
+    RETURN;
+END;
+$func$ LANGUAGE plpgsql;
+
+CREATE OR REPLACE FUNCTION action.item_user_circ_test( circ_ou INT, match_item BIGINT, match_user INT, renewal BOOL ) RETURNS SETOF action.circ_matrix_test_result AS $func$
+DECLARE
+    user_object             actor.usr%ROWTYPE;
+    standing_penalty        config.standing_penalty%ROWTYPE;
+    item_object             asset.copy%ROWTYPE;
+    item_status_object      config.copy_status%ROWTYPE;
+    item_location_object    asset.copy_location%ROWTYPE;
+    result                  action.circ_matrix_test_result;
+    circ_test               action.found_circ_matrix_matchpoint;
+    circ_matchpoint         config.circ_matrix_matchpoint%ROWTYPE;
+    circ_limit_set          config.circ_limit_set%ROWTYPE;
+    hold_ratio              action.hold_stats%ROWTYPE;
+    penalty_type            TEXT;
+    items_out               INT;
+    context_org_list        INT[];
+    done                    BOOL := FALSE;
+    item_prox               INT;
+    home_prox               INT;
+BEGIN
+    -- Assume success unless we hit a failure condition
+    result.success := TRUE;
+
+    -- Need user info to look up matchpoints
+    SELECT INTO user_object * FROM actor.usr WHERE id = match_user AND NOT deleted;
+
+    -- (Insta)Fail if we couldn't find the user
+    IF user_object.id IS NULL THEN
+        result.fail_part := 'no_user';
+        result.success := FALSE;
+        done := TRUE;
+        RETURN NEXT result;
+        RETURN;
+    END IF;
+
+    -- Need item info to look up matchpoints
+    SELECT INTO item_object * FROM asset.copy WHERE id = match_item AND NOT deleted;
+
+    -- (Insta)Fail if we couldn't find the item 
+    IF item_object.id IS NULL THEN
+        result.fail_part := 'no_item';
+        result.success := FALSE;
+        done := TRUE;
+        RETURN NEXT result;
+        RETURN;
+    END IF;
+
+    SELECT INTO circ_test * FROM action.find_circ_matrix_matchpoint(circ_ou, item_object, user_object, renewal);
+
+    circ_matchpoint             := circ_test.matchpoint;
+    result.matchpoint           := circ_matchpoint.id;
+    result.circulate            := circ_matchpoint.circulate;
+    result.duration_rule        := circ_matchpoint.duration_rule;
+    result.recurring_fine_rule  := circ_matchpoint.recurring_fine_rule;
+    result.max_fine_rule        := circ_matchpoint.max_fine_rule;
+    result.hard_due_date        := circ_matchpoint.hard_due_date;
+    result.renewals             := circ_matchpoint.renewals;
+    result.grace_period         := circ_matchpoint.grace_period;
+    result.buildrows            := circ_test.buildrows;
+
+    -- (Insta)Fail if we couldn't find a matchpoint
+    IF circ_test.success = false THEN
+        result.fail_part := 'no_matchpoint';
+        result.success := FALSE;
+        done := TRUE;
+        RETURN NEXT result;
+        RETURN;
+    END IF;
+
+    -- All failures before this point are non-recoverable
+    -- Below this point are possibly overridable failures
+
+    -- Fail if the user is barred
+    IF user_object.barred IS TRUE THEN
+        result.fail_part := 'actor.usr.barred';
+        result.success := FALSE;
+        done := TRUE;
+        RETURN NEXT result;
+    END IF;
+
+    -- Fail if the item can't circulate
+    IF item_object.circulate IS FALSE THEN
+        result.fail_part := 'asset.copy.circulate';
+        result.success := FALSE;
+        done := TRUE;
+        RETURN NEXT result;
+    END IF;
+
+    -- Fail if the item isn't in a circulateable status on a non-renewal
+    IF NOT renewal AND item_object.status NOT IN ( 0, 7, 8 ) THEN 
+        result.fail_part := 'asset.copy.status';
+        result.success := FALSE;
+        done := TRUE;
+        RETURN NEXT result;
+    -- Alternately, fail if the item isn't checked out on a renewal
+    ELSIF renewal AND item_object.status <> 1 THEN
+        result.fail_part := 'asset.copy.status';
+        result.success := FALSE;
+        done := TRUE;
+        RETURN NEXT result;
+    END IF;
+
+    -- Fail if the item can't circulate because of the shelving location
+    SELECT INTO item_location_object * FROM asset.copy_location WHERE id = item_object.location;
+    IF item_location_object.circulate IS FALSE THEN
+        result.fail_part := 'asset.copy_location.circulate';
+        result.success := FALSE;
+        done := TRUE;
+        RETURN NEXT result;
+    END IF;
+
+    -- Use Circ OU for penalties and such
+    SELECT INTO context_org_list ARRAY_AGG(id) FROM actor.org_unit_full_path( circ_ou );
+
+    -- Proximity of user's home_ou to circ_ou to see if penalties should be ignored.
+    SELECT INTO home_prox prox FROM actor.org_unit_proximity WHERE from_org = user_object.home_ou AND to_org = circ_ou;
+
+    -- Proximity of user's home_ou to item circ_lib to see if penalties should be ignored.
+    SELECT INTO item_prox prox FROM actor.org_unit_proximity WHERE from_org = user_object.home_ou AND to_org = item_object.circ_lib;
+
+    IF renewal THEN
+        penalty_type = '%RENEW%';
+    ELSE
+        penalty_type = '%CIRC%';
+    END IF;
+
+    FOR standing_penalty IN
+        SELECT  DISTINCT csp.*
+          FROM  actor.usr_standing_penalty usp
+                JOIN config.standing_penalty csp ON (csp.id = usp.standing_penalty)
+          WHERE usr = match_user
+                AND usp.org_unit IN ( SELECT * FROM unnest(context_org_list) )
+                AND (usp.stop_date IS NULL or usp.stop_date > NOW())
+                AND (csp.ignore_proximity IS NULL
+                     OR csp.ignore_proximity < home_prox
+                     OR csp.ignore_proximity < item_prox)
+                AND csp.block_list LIKE penalty_type LOOP
+
+        result.fail_part := standing_penalty.name;
+        result.success := FALSE;
+        done := TRUE;
+        RETURN NEXT result;
+    END LOOP;
+
+    -- Fail if the test is set to hard non-circulating
+    IF circ_matchpoint.circulate IS FALSE THEN
+        result.fail_part := 'config.circ_matrix_test.circulate';
+        result.success := FALSE;
+        done := TRUE;
+        RETURN NEXT result;
+    END IF;
+
+    -- Fail if the total copy-hold ratio is too low
+    IF circ_matchpoint.total_copy_hold_ratio IS NOT NULL THEN
+        SELECT INTO hold_ratio * FROM action.copy_related_hold_stats(match_item);
+        IF hold_ratio.total_copy_ratio IS NOT NULL AND hold_ratio.total_copy_ratio < circ_matchpoint.total_copy_hold_ratio THEN
+            result.fail_part := 'config.circ_matrix_test.total_copy_hold_ratio';
+            result.success := FALSE;
+            done := TRUE;
+            RETURN NEXT result;
+        END IF;
+    END IF;
+
+    -- Fail if the available copy-hold ratio is too low
+    IF circ_matchpoint.available_copy_hold_ratio IS NOT NULL THEN
+        IF hold_ratio.hold_count IS NULL THEN
+            SELECT INTO hold_ratio * FROM action.copy_related_hold_stats(match_item);
+        END IF;
+        IF hold_ratio.available_copy_ratio IS NOT NULL AND hold_ratio.available_copy_ratio < circ_matchpoint.available_copy_hold_ratio THEN
+            result.fail_part := 'config.circ_matrix_test.available_copy_hold_ratio';
+            result.success := FALSE;
+            done := TRUE;
+            RETURN NEXT result;
+        END IF;
+    END IF;
+
+    -- Fail if the user has too many items out by defined limit sets
+    FOR circ_limit_set IN SELECT ccls.* FROM config.circ_limit_set ccls
+      JOIN config.circ_matrix_limit_set_map ccmlsm ON ccmlsm.limit_set = ccls.id
+      WHERE ccmlsm.active AND ( ccmlsm.matchpoint = circ_matchpoint.id OR
+        ( ccmlsm.matchpoint IN (SELECT * FROM unnest(result.buildrows)) AND ccmlsm.fallthrough )
+        ) LOOP
+            IF circ_limit_set.items_out > 0 AND NOT renewal THEN
+                SELECT INTO context_org_list ARRAY_AGG(aou.id)
+                  FROM actor.org_unit_full_path( circ_ou ) aou
+                    JOIN actor.org_unit_type aout ON aou.ou_type = aout.id
+                  WHERE aout.depth >= circ_limit_set.depth;
+                IF circ_limit_set.global THEN
+                    WITH RECURSIVE descendant_depth AS (
+                        SELECT  ou.id,
+                            ou.parent_ou
+                        FROM  actor.org_unit ou
+                        WHERE ou.id IN (SELECT * FROM unnest(context_org_list))
+                            UNION
+                        SELECT  ou.id,
+                            ou.parent_ou
+                        FROM  actor.org_unit ou
+                            JOIN descendant_depth ot ON (ot.id = ou.parent_ou)
+                    ) SELECT INTO context_org_list ARRAY_AGG(ou.id) FROM actor.org_unit ou JOIN descendant_depth USING (id);
+                END IF;
+                SELECT INTO items_out COUNT(DISTINCT circ.id)
+                  FROM action.circulation circ
+                    JOIN asset.copy copy ON (copy.id = circ.target_copy)
+                    LEFT JOIN action.circulation_limit_group_map aclgm ON (circ.id = aclgm.circ)
+                  WHERE circ.usr = match_user
+                    AND circ.circ_lib IN (SELECT * FROM unnest(context_org_list))
+                    AND circ.checkin_time IS NULL
+                    AND (circ.stop_fines IN ('MAXFINES','LONGOVERDUE') OR circ.stop_fines IS NULL)
+                    AND (copy.circ_modifier IN (SELECT circ_mod FROM config.circ_limit_set_circ_mod_map WHERE limit_set = circ_limit_set.id)
+                        OR copy.location IN (SELECT copy_loc FROM config.circ_limit_set_copy_loc_map WHERE limit_set = circ_limit_set.id)
+                        OR aclgm.limit_group IN (SELECT limit_group FROM config.circ_limit_set_group_map WHERE limit_set = circ_limit_set.id)
+                    );
+                IF items_out >= circ_limit_set.items_out THEN
+                    result.fail_part := 'config.circ_matrix_circ_mod_test';
+                    result.success := FALSE;
+                    done := TRUE;
+                    RETURN NEXT result;
+                END IF;
+            END IF;
+            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;
+    END LOOP;
+
+    -- If we passed everything, return the successful matchpoint
+    IF NOT done THEN
+        RETURN NEXT result;
+    END IF;
+
+    RETURN;
+END;
+$func$ LANGUAGE plpgsql;
+
+COMMIT;
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.config.standing_penalty.ignore_proximity.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.config.standing_penalty.ignore_proximity.sql
deleted file mode 100644 (file)
index 615350e..0000000
+++ /dev/null
@@ -1,478 +0,0 @@
-BEGIN;
-
---SELECT evergreen.upgrade_deps_block_check('XXXX', :eg_version);
-
-ALTER TABLE config.standing_penalty
-      ADD COLUMN ignore_proximity INTEGER;
-
-CREATE OR REPLACE FUNCTION action.hold_request_permit_test( pickup_ou INT, request_ou INT, match_item BIGINT, match_user INT, match_requestor INT, retargetting BOOL ) RETURNS SETOF action.matrix_test_result AS $func$
-DECLARE
-    matchpoint_id        INT;
-    user_object        actor.usr%ROWTYPE;
-    age_protect_object    config.rule_age_hold_protect%ROWTYPE;
-    standing_penalty    config.standing_penalty%ROWTYPE;
-    transit_range_ou_type    actor.org_unit_type%ROWTYPE;
-    transit_source        actor.org_unit%ROWTYPE;
-    item_object        asset.copy%ROWTYPE;
-    item_cn_object     asset.call_number%ROWTYPE;
-    item_status_object  config.copy_status%ROWTYPE;
-    item_location_object    asset.copy_location%ROWTYPE;
-    ou_skip              actor.org_unit_setting%ROWTYPE;
-    result            action.matrix_test_result;
-    hold_test        config.hold_matrix_matchpoint%ROWTYPE;
-    use_active_date   TEXT;
-    age_protect_date  TIMESTAMP WITH TIME ZONE;
-    hold_count        INT;
-    hold_transit_prox    INT;
-    frozen_hold_count    INT;
-    context_org_list    INT[];
-    done            BOOL := FALSE;
-    hold_penalty TEXT;
-    v_pickup_ou ALIAS FOR pickup_ou;
-    v_request_ou ALIAS FOR request_ou;
-    item_prox INT;
-    pickup_prox INT;
-BEGIN
-    SELECT INTO user_object * FROM actor.usr WHERE id = match_user;
-    SELECT INTO context_org_list ARRAY_AGG(id) FROM actor.org_unit_full_path( v_pickup_ou );
-
-    result.success := TRUE;
-
-    -- The HOLD penalty block only applies to new holds.
-    -- The CAPTURE penalty block applies to existing holds.
-    hold_penalty := 'HOLD';
-    IF retargetting THEN
-        hold_penalty := 'CAPTURE';
-    END IF;
-
-    -- Fail if we couldn't find a user
-    IF user_object.id IS NULL THEN
-        result.fail_part := 'no_user';
-        result.success := FALSE;
-        done := TRUE;
-        RETURN NEXT result;
-        RETURN;
-    END IF;
-
-    SELECT INTO item_object * FROM asset.copy WHERE id = match_item;
-
-    -- Fail if we couldn't find a copy
-    IF item_object.id IS NULL THEN
-        result.fail_part := 'no_item';
-        result.success := FALSE;
-        done := TRUE;
-        RETURN NEXT result;
-        RETURN;
-    END IF;
-
-    SELECT INTO matchpoint_id action.find_hold_matrix_matchpoint(v_pickup_ou, v_request_ou, match_item, match_user, match_requestor);
-    result.matchpoint := matchpoint_id;
-
-    SELECT INTO ou_skip * FROM actor.org_unit_setting WHERE name = 'circ.holds.target_skip_me' AND org_unit = item_object.circ_lib;
-
-    -- Fail if the circ_lib for the item has circ.holds.target_skip_me set to true
-    IF ou_skip.id IS NOT NULL AND ou_skip.value = 'true' THEN
-        result.fail_part := 'circ.holds.target_skip_me';
-        result.success := FALSE;
-        done := TRUE;
-        RETURN NEXT result;
-        RETURN;
-    END IF;
-
-    -- Fail if user is barred
-    IF user_object.barred IS TRUE THEN
-        result.fail_part := 'actor.usr.barred';
-        result.success := FALSE;
-        done := TRUE;
-        RETURN NEXT result;
-        RETURN;
-    END IF;
-
-    SELECT INTO item_cn_object * FROM asset.call_number WHERE id = item_object.call_number;
-    SELECT INTO item_status_object * FROM config.copy_status WHERE id = item_object.status;
-    SELECT INTO item_location_object * FROM asset.copy_location WHERE id = item_object.location;
-
-    -- Fail if we couldn't find any matchpoint (requires a default)
-    IF matchpoint_id IS NULL THEN
-        result.fail_part := 'no_matchpoint';
-        result.success := FALSE;
-        done := TRUE;
-        RETURN NEXT result;
-        RETURN;
-    END IF;
-
-    SELECT INTO hold_test * FROM config.hold_matrix_matchpoint WHERE id = matchpoint_id;
-
-    IF hold_test.holdable IS FALSE THEN
-        result.fail_part := 'config.hold_matrix_test.holdable';
-        result.success := FALSE;
-        done := TRUE;
-        RETURN NEXT result;
-    END IF;
-
-    IF item_object.holdable IS FALSE THEN
-        result.fail_part := 'item.holdable';
-        result.success := FALSE;
-        done := TRUE;
-        RETURN NEXT result;
-    END IF;
-
-    IF item_status_object.holdable IS FALSE THEN
-        result.fail_part := 'status.holdable';
-        result.success := FALSE;
-        done := TRUE;
-        RETURN NEXT result;
-    END IF;
-
-    IF item_location_object.holdable IS FALSE THEN
-        result.fail_part := 'location.holdable';
-        result.success := FALSE;
-        done := TRUE;
-        RETURN NEXT result;
-    END IF;
-
-    IF hold_test.transit_range IS NOT NULL THEN
-        SELECT INTO transit_range_ou_type * FROM actor.org_unit_type WHERE id = hold_test.transit_range;
-        IF hold_test.distance_is_from_owner THEN
-            SELECT INTO transit_source ou.* FROM actor.org_unit ou JOIN asset.call_number cn ON (cn.owning_lib = ou.id) WHERE cn.id = item_object.call_number;
-        ELSE
-            SELECT INTO transit_source * FROM actor.org_unit WHERE id = item_object.circ_lib;
-        END IF;
-
-        PERFORM * FROM actor.org_unit_descendants( transit_source.id, transit_range_ou_type.depth ) WHERE id = v_pickup_ou;
-
-        IF NOT FOUND THEN
-            result.fail_part := 'transit_range';
-            result.success := FALSE;
-            done := TRUE;
-            RETURN NEXT result;
-        END IF;
-    END IF;
-    -- Proximity of user's home_ou to the pickup_lib to see if penalty should be ignored.
-    SELECT INTO pickup_prox prox FROM actor.org_unit_proximity WHERE from_org = user_object.home_ou AND to_org = v_pickup_ou;
-    -- Proximity of user's home_ou to the items' lib to see if penalty should be ignored.
-    IF hold_test.distance_is_from_owner THEN
-        SELECT INTO item_prox prox FROM actor.org_unit_proximity WHERE from_org = user_object.home_ou AND to_org = item_cn_object.owning_lib;
-    ELSE
-        SELECT INTO item_prox prox FROM actor.org_unit_proximity WHERE from_org = user_object.home_ou AND to_org = item_object.circ_lib;
-    END IF;
-
-    FOR standing_penalty IN
-        SELECT  DISTINCT csp.*
-          FROM  actor.usr_standing_penalty usp
-                JOIN config.standing_penalty csp ON (csp.id = usp.standing_penalty)
-          WHERE usr = match_user
-                AND usp.org_unit IN ( SELECT * FROM unnest(context_org_list) )
-                AND (usp.stop_date IS NULL or usp.stop_date > NOW())
-                AND (csp.ignore_proximity IS NULL OR csp.ignore_proximity < item_prox
-                     OR csp.ignore_proximity < pickup_prox)
-                AND csp.block_list LIKE '%' || hold_penalty || '%' LOOP
-
-        result.fail_part := standing_penalty.name;
-        result.success := FALSE;
-        done := TRUE;
-        RETURN NEXT result;
-    END LOOP;
-
-    IF hold_test.stop_blocked_user IS TRUE THEN
-        FOR standing_penalty IN
-            SELECT  DISTINCT csp.*
-              FROM  actor.usr_standing_penalty usp
-                    JOIN config.standing_penalty csp ON (csp.id = usp.standing_penalty)
-              WHERE usr = match_user
-                    AND usp.org_unit IN ( SELECT * FROM unnest(context_org_list) )
-                    AND (usp.stop_date IS NULL or usp.stop_date > NOW())
-                    AND csp.block_list LIKE '%CIRC%' LOOP
-    
-            result.fail_part := standing_penalty.name;
-            result.success := FALSE;
-            done := TRUE;
-            RETURN NEXT result;
-        END LOOP;
-    END IF;
-
-    IF hold_test.max_holds IS NOT NULL AND NOT retargetting THEN
-        SELECT    INTO hold_count COUNT(*)
-          FROM    action.hold_request
-          WHERE    usr = match_user
-            AND fulfillment_time IS NULL
-            AND cancel_time IS NULL
-            AND CASE WHEN hold_test.include_frozen_holds THEN TRUE ELSE frozen IS FALSE END;
-
-        IF hold_count >= hold_test.max_holds THEN
-            result.fail_part := 'config.hold_matrix_test.max_holds';
-            result.success := FALSE;
-            done := TRUE;
-            RETURN NEXT result;
-        END IF;
-    END IF;
-
-    IF item_object.age_protect IS NOT NULL THEN
-        SELECT INTO age_protect_object * FROM config.rule_age_hold_protect WHERE id = item_object.age_protect;
-        IF hold_test.distance_is_from_owner THEN
-            SELECT INTO use_active_date value FROM actor.org_unit_ancestor_setting('circ.holds.age_protect.active_date', item_cn_object.owning_lib);
-        ELSE
-            SELECT INTO use_active_date value FROM actor.org_unit_ancestor_setting('circ.holds.age_protect.active_date', item_object.circ_lib);
-        END IF;
-        IF use_active_date = 'true' THEN
-            age_protect_date := COALESCE(item_object.active_date, NOW());
-        ELSE
-            age_protect_date := item_object.create_date;
-        END IF;
-        IF age_protect_date + age_protect_object.age > NOW() THEN
-            IF hold_test.distance_is_from_owner THEN
-                SELECT INTO item_cn_object * FROM asset.call_number WHERE id = item_object.call_number;
-                SELECT INTO hold_transit_prox prox FROM actor.org_unit_proximity WHERE from_org = item_cn_object.owning_lib AND to_org = v_pickup_ou;
-            ELSE
-                SELECT INTO hold_transit_prox prox FROM actor.org_unit_proximity WHERE from_org = item_object.circ_lib AND to_org = v_pickup_ou;
-            END IF;
-
-            IF hold_transit_prox > age_protect_object.prox THEN
-                result.fail_part := 'config.rule_age_hold_protect.prox';
-                result.success := FALSE;
-                done := TRUE;
-                RETURN NEXT result;
-            END IF;
-        END IF;
-    END IF;
-
-    IF NOT done THEN
-        RETURN NEXT result;
-    END IF;
-
-    RETURN;
-END;
-$func$ LANGUAGE plpgsql;
-
-CREATE OR REPLACE FUNCTION action.item_user_circ_test( circ_ou INT, match_item BIGINT, match_user INT, renewal BOOL ) RETURNS SETOF action.circ_matrix_test_result AS $func$
-DECLARE
-    user_object             actor.usr%ROWTYPE;
-    standing_penalty        config.standing_penalty%ROWTYPE;
-    item_object             asset.copy%ROWTYPE;
-    item_status_object      config.copy_status%ROWTYPE;
-    item_location_object    asset.copy_location%ROWTYPE;
-    result                  action.circ_matrix_test_result;
-    circ_test               action.found_circ_matrix_matchpoint;
-    circ_matchpoint         config.circ_matrix_matchpoint%ROWTYPE;
-    circ_limit_set          config.circ_limit_set%ROWTYPE;
-    hold_ratio              action.hold_stats%ROWTYPE;
-    penalty_type            TEXT;
-    items_out               INT;
-    context_org_list        INT[];
-    done                    BOOL := FALSE;
-    item_prox               INT;
-    home_prox               INT;
-BEGIN
-    -- Assume success unless we hit a failure condition
-    result.success := TRUE;
-
-    -- Need user info to look up matchpoints
-    SELECT INTO user_object * FROM actor.usr WHERE id = match_user AND NOT deleted;
-
-    -- (Insta)Fail if we couldn't find the user
-    IF user_object.id IS NULL THEN
-        result.fail_part := 'no_user';
-        result.success := FALSE;
-        done := TRUE;
-        RETURN NEXT result;
-        RETURN;
-    END IF;
-
-    -- Need item info to look up matchpoints
-    SELECT INTO item_object * FROM asset.copy WHERE id = match_item AND NOT deleted;
-
-    -- (Insta)Fail if we couldn't find the item 
-    IF item_object.id IS NULL THEN
-        result.fail_part := 'no_item';
-        result.success := FALSE;
-        done := TRUE;
-        RETURN NEXT result;
-        RETURN;
-    END IF;
-
-    SELECT INTO circ_test * FROM action.find_circ_matrix_matchpoint(circ_ou, item_object, user_object, renewal);
-
-    circ_matchpoint             := circ_test.matchpoint;
-    result.matchpoint           := circ_matchpoint.id;
-    result.circulate            := circ_matchpoint.circulate;
-    result.duration_rule        := circ_matchpoint.duration_rule;
-    result.recurring_fine_rule  := circ_matchpoint.recurring_fine_rule;
-    result.max_fine_rule        := circ_matchpoint.max_fine_rule;
-    result.hard_due_date        := circ_matchpoint.hard_due_date;
-    result.renewals             := circ_matchpoint.renewals;
-    result.grace_period         := circ_matchpoint.grace_period;
-    result.buildrows            := circ_test.buildrows;
-
-    -- (Insta)Fail if we couldn't find a matchpoint
-    IF circ_test.success = false THEN
-        result.fail_part := 'no_matchpoint';
-        result.success := FALSE;
-        done := TRUE;
-        RETURN NEXT result;
-        RETURN;
-    END IF;
-
-    -- All failures before this point are non-recoverable
-    -- Below this point are possibly overridable failures
-
-    -- Fail if the user is barred
-    IF user_object.barred IS TRUE THEN
-        result.fail_part := 'actor.usr.barred';
-        result.success := FALSE;
-        done := TRUE;
-        RETURN NEXT result;
-    END IF;
-
-    -- Fail if the item can't circulate
-    IF item_object.circulate IS FALSE THEN
-        result.fail_part := 'asset.copy.circulate';
-        result.success := FALSE;
-        done := TRUE;
-        RETURN NEXT result;
-    END IF;
-
-    -- Fail if the item isn't in a circulateable status on a non-renewal
-    IF NOT renewal AND item_object.status NOT IN ( 0, 7, 8 ) THEN 
-        result.fail_part := 'asset.copy.status';
-        result.success := FALSE;
-        done := TRUE;
-        RETURN NEXT result;
-    -- Alternately, fail if the item isn't checked out on a renewal
-    ELSIF renewal AND item_object.status <> 1 THEN
-        result.fail_part := 'asset.copy.status';
-        result.success := FALSE;
-        done := TRUE;
-        RETURN NEXT result;
-    END IF;
-
-    -- Fail if the item can't circulate because of the shelving location
-    SELECT INTO item_location_object * FROM asset.copy_location WHERE id = item_object.location;
-    IF item_location_object.circulate IS FALSE THEN
-        result.fail_part := 'asset.copy_location.circulate';
-        result.success := FALSE;
-        done := TRUE;
-        RETURN NEXT result;
-    END IF;
-
-    -- Use Circ OU for penalties and such
-    SELECT INTO context_org_list ARRAY_AGG(id) FROM actor.org_unit_full_path( circ_ou );
-
-    -- Proximity of user's home_ou to circ_ou to see if penalties should be ignored.
-    SELECT INTO home_prox prox FROM actor.org_unit_proximity WHERE from_org = user_object.home_ou AND to_org = circ_ou;
-
-    -- Proximity of user's home_ou to item circ_lib to see if penalties should be ignored.
-    SELECT INTO item_prox prox FROM actor.org_unit_proximity WHERE from_org = user_object.home_ou AND to_org = item_object.circ_lib;
-
-    IF renewal THEN
-        penalty_type = '%RENEW%';
-    ELSE
-        penalty_type = '%CIRC%';
-    END IF;
-
-    FOR standing_penalty IN
-        SELECT  DISTINCT csp.*
-          FROM  actor.usr_standing_penalty usp
-                JOIN config.standing_penalty csp ON (csp.id = usp.standing_penalty)
-          WHERE usr = match_user
-                AND usp.org_unit IN ( SELECT * FROM unnest(context_org_list) )
-                AND (usp.stop_date IS NULL or usp.stop_date > NOW())
-                AND (csp.ignore_proximity IS NULL
-                     OR csp.ignore_proximity < home_prox
-                     OR csp.ignore_proximity < item_prox)
-                AND csp.block_list LIKE penalty_type LOOP
-
-        result.fail_part := standing_penalty.name;
-        result.success := FALSE;
-        done := TRUE;
-        RETURN NEXT result;
-    END LOOP;
-
-    -- Fail if the test is set to hard non-circulating
-    IF circ_matchpoint.circulate IS FALSE THEN
-        result.fail_part := 'config.circ_matrix_test.circulate';
-        result.success := FALSE;
-        done := TRUE;
-        RETURN NEXT result;
-    END IF;
-
-    -- Fail if the total copy-hold ratio is too low
-    IF circ_matchpoint.total_copy_hold_ratio IS NOT NULL THEN
-        SELECT INTO hold_ratio * FROM action.copy_related_hold_stats(match_item);
-        IF hold_ratio.total_copy_ratio IS NOT NULL AND hold_ratio.total_copy_ratio < circ_matchpoint.total_copy_hold_ratio THEN
-            result.fail_part := 'config.circ_matrix_test.total_copy_hold_ratio';
-            result.success := FALSE;
-            done := TRUE;
-            RETURN NEXT result;
-        END IF;
-    END IF;
-
-    -- Fail if the available copy-hold ratio is too low
-    IF circ_matchpoint.available_copy_hold_ratio IS NOT NULL THEN
-        IF hold_ratio.hold_count IS NULL THEN
-            SELECT INTO hold_ratio * FROM action.copy_related_hold_stats(match_item);
-        END IF;
-        IF hold_ratio.available_copy_ratio IS NOT NULL AND hold_ratio.available_copy_ratio < circ_matchpoint.available_copy_hold_ratio THEN
-            result.fail_part := 'config.circ_matrix_test.available_copy_hold_ratio';
-            result.success := FALSE;
-            done := TRUE;
-            RETURN NEXT result;
-        END IF;
-    END IF;
-
-    -- Fail if the user has too many items out by defined limit sets
-    FOR circ_limit_set IN SELECT ccls.* FROM config.circ_limit_set ccls
-      JOIN config.circ_matrix_limit_set_map ccmlsm ON ccmlsm.limit_set = ccls.id
-      WHERE ccmlsm.active AND ( ccmlsm.matchpoint = circ_matchpoint.id OR
-        ( ccmlsm.matchpoint IN (SELECT * FROM unnest(result.buildrows)) AND ccmlsm.fallthrough )
-        ) LOOP
-            IF circ_limit_set.items_out > 0 AND NOT renewal THEN
-                SELECT INTO context_org_list ARRAY_AGG(aou.id)
-                  FROM actor.org_unit_full_path( circ_ou ) aou
-                    JOIN actor.org_unit_type aout ON aou.ou_type = aout.id
-                  WHERE aout.depth >= circ_limit_set.depth;
-                IF circ_limit_set.global THEN
-                    WITH RECURSIVE descendant_depth AS (
-                        SELECT  ou.id,
-                            ou.parent_ou
-                        FROM  actor.org_unit ou
-                        WHERE ou.id IN (SELECT * FROM unnest(context_org_list))
-                            UNION
-                        SELECT  ou.id,
-                            ou.parent_ou
-                        FROM  actor.org_unit ou
-                            JOIN descendant_depth ot ON (ot.id = ou.parent_ou)
-                    ) SELECT INTO context_org_list ARRAY_AGG(ou.id) FROM actor.org_unit ou JOIN descendant_depth USING (id);
-                END IF;
-                SELECT INTO items_out COUNT(DISTINCT circ.id)
-                  FROM action.circulation circ
-                    JOIN asset.copy copy ON (copy.id = circ.target_copy)
-                    LEFT JOIN action.circulation_limit_group_map aclgm ON (circ.id = aclgm.circ)
-                  WHERE circ.usr = match_user
-                    AND circ.circ_lib IN (SELECT * FROM unnest(context_org_list))
-                    AND circ.checkin_time IS NULL
-                    AND (circ.stop_fines IN ('MAXFINES','LONGOVERDUE') OR circ.stop_fines IS NULL)
-                    AND (copy.circ_modifier IN (SELECT circ_mod FROM config.circ_limit_set_circ_mod_map WHERE limit_set = circ_limit_set.id)
-                        OR copy.location IN (SELECT copy_loc FROM config.circ_limit_set_copy_loc_map WHERE limit_set = circ_limit_set.id)
-                        OR aclgm.limit_group IN (SELECT limit_group FROM config.circ_limit_set_group_map WHERE limit_set = circ_limit_set.id)
-                    );
-                IF items_out >= circ_limit_set.items_out THEN
-                    result.fail_part := 'config.circ_matrix_circ_mod_test';
-                    result.success := FALSE;
-                    done := TRUE;
-                    RETURN NEXT result;
-                END IF;
-            END IF;
-            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;
-    END LOOP;
-
-    -- If we passed everything, return the successful matchpoint
-    IF NOT done THEN
-        RETURN NEXT result;
-    END IF;
-
-    RETURN;
-END;
-$func$ LANGUAGE plpgsql;
-
-COMMIT;
index 7bdc5b8..08ef46a 100644 (file)
@@ -26,5 +26,5 @@ you set the ignore_proximity to 0 on patron exceeds overdue fines,
 then the patron will still be able to place holds on and checkout
 copies owned by their home organizational unit at their home
 organizational unit.  They will not, however, be able to receive
-copies form other organizational units, nor use other organizational
+copies from other organizational units, nor use other organizational
 units as a patron.