Theory:
Make the things returned (circulate, duration_rule, recurring_fine_rule, max_fine_rule) "fall through" when set to NULL.

The attached patch attempts to make this happen on the back end, but provides no front end interface changes for configuring it.

IT HAS NOT BEEN TESTED, mainly because I don't want to screw with our test system right now when others may be trying to test existing functionality.

It also adds in the ability to pass in one more (optional) boolean parameter to the function to return the entire list of rules used to create the final result, intended for "debug/test" front end functionality to show what rules were considered in the fall-through checking.

Pros:

You can override a subset of those fields with a specific rule while allowing broader rules to fill in the holes. This may result in less duplication of information across rules, making things easier to maintain. Thus, this may result in less rules in general, and thus less processing time on sorting them overall.

Cons:

Manually figuring out the specifics of what will happen will take more time/effort.
Changing a single rule may have a greater unintended effect on other rules.
Staff would need training for when to have a rule fall through and when to set it. More time to return from the DB for any rule that is "falling through" to broader rules.

Examples for the following org tree:

CONS
-SYSA
--LIBC
--LIBD
-SYSB
--LIBE
--LIBF

Implementing the following "business" rules:

At the CONS level:
By default, everything circulates, uses DFLT_DUR duration, DFLT_RFINE recurring fine, and DFLT_MFINE max fine.
Circ Modifier "book" uses the duration BOOK_DUR
Reference flagged materials don't circulate

At the SYSA level there are no special rules.

At the SYSB level the max fine should be SYSB_MFINE.

At the LIBC level the recurring fine is LIBC_RFINE

At the LIBD level circ modifier "book" uses the DFLT_DUR duration instead of "BOOK_DUR"

At the LIBE level reference flagged materials circulate.

At the LIBF level there are no special rules.

The current method would require the following circ rules to implement those business rules:

CIRC_LIB CIRC_MOD REFERENCE CIRC? DURATION_RULE RECURRING_FINE MAX_FINE
CONS     NULL     NULL      TRUE  DFLT_DUR      DFLT_RFINE     DFLT_MFINE
CONS     NULL     TRUE      FALSE DFLT_DUR      DFLT_RFINE     DFLT_MFINE
CONS     book     NULL      TRUE  BOOK_DUR      DFLT_RFINE     DFLT_MFINE
CONS     book     TRUE      FALSE BOOK_DUR      DFLT_RFINE     DFLT_MFINE
SYSB     NULL     NULL      TRUE  DFLT_DUR      DFLT_RFINE     SYSB_MFINE
SYSB     NULL     TRUE      FALSE DFLT_DUR      DFLT_RFINE     SYSB_MFINE
SYSB     book     NULL      TRUE  BOOK_DUR      DFLT_RFINE     SYSB_MFINE
SYSB     book     TRUE      FALSE BOOK_DUR      DFLT_RFINE     SYSB_MFINE
LIBC     NULL     NULL      TRUE  DFLT_DUR      LIBC_RFINE     DFLT_MFINE
LIBC     NULL     TRUE      FALSE DFLT_DUR      LIBC_RFINE     DFLT_MFINE
LIBC     book     NULL      TRUE  BOOK_DUR      LIBC_RFINE     DFLT_MFINE
LIBC     book     TRUE      FALSE BOOK_DUR      LIBC_RFINE     DFLT_MFINE
LIBD     book     NULL      TRUE  DFLT_DUR      DFLT_RFINE     DFLT_MFINE
LIBD     book     TRUE      FALSE DFLT_DUR      DFLT_RFINE     DFLT_MFINE
LIBE     NULL     NULL      TRUE  DFLT_DUR      DFLT_RFINE     SYSB_MFINE
LIBE     book     NULL      TRUE  BOOK_DUR      DFLT_RFINE     SYSB_MFINE

16 circ rules total.

The new method would require the following circ rules to implement those business rules:

CIRC_LIB CIRC_MOD REFERENCE CIRC? DURATION_RULE RECURRING_FINE MAX_FINE
CONS     NULL     NULL      TRUE  DFLT_DUR      DFLT_RFINE     DFLT_MFINE
CONS     book     NULL      NULL  BOOK_DUR      NULL           NULL
CONS     NULL     TRUE      FALSE NULL          NULL           NULL
SYSB     NULL     NULL      NULL  NULL          NULL           SYSB_MFINE
LIBC     NULL     NULL      NULL  NULL          LIBC_RFINE     NULL
LIBD     book     NULL      NULL  DFLT_DUR      NULL           NULL
LIBE     NULL     TRUE      TRUE  NULL          NULL           NULL

7 circ rules total.

Starting with the above, lets assume that SYSA wants to change their recurring fine to SYSA_RFINE.
LIBC's recurring fine is to be unchanged.

The current method requires the following changes:

ADD the following entries:
CIRC_LIB CIRC_MOD REFERENCE CIRC? DURATION_RULE RECURRING_FINE MAX_FINE
SYSA     NULL     NULL      TRUE  DFLT_DUR      SYSA_RFINE     DFLT_MFINE
SYSA     NULL     TRUE      FALSE DFLT_DUR      SYSA_RFINE     DFLT_MFINE
SYSA     book     NULL      TRUE  BOOK_DUR      SYSA_RFINE     DFLT_MFINE
SYSA     book     TRUE      FALSE BOOK_DUR      SYSA_RFINE     DFLT_MFINE

UPDATE the LIBD entries:
CIRC_LIB CIRC_MOD REFERENCE CIRC? DURATION_RULE RECURRING_FINE MAX_FINE
LIBD     book     NULL      TRUE  DFLT_DUR      SYSA_RFINE     DFLT_MFINE
LIBD     book     TRUE      FALSE DFLT_DUR      SYSA_RFINE     DFLT_MFINE

4 rules added, 2 changed, total is now 20 rules.

The new method would require the following changes:

ADD the following entry:
CIRC_LIB CIRC_MOD REFERENCE CIRC? DURATION_RULE RECURRING_FINE MAX_FINE
SYSA     NULL     NULL      NULL  NULL          SYSA_RFINE     NULL

1 rule added, 0 changed, total is now 8 rules.

Thomas Berezansky
Merrimack Valley Library Consortium
Index: Open-ILS/src/sql/Pg/100.circ_matrix.sql
===================================================================
--- Open-ILS/src/sql/Pg/100.circ_matrix.sql	(revision 17478)
+++ Open-ILS/src/sql/Pg/100.circ_matrix.sql	(working copy)
@@ -106,10 +106,10 @@
     is_renewal           BOOL,
     usr_age_lower_bound  INTERVAL,
     usr_age_upper_bound  INTERVAL,
-    circulate            BOOL    NOT NULL DEFAULT TRUE,    -- Hard "can't circ" flag requiring an override
-    duration_rule        INT     NOT NULL REFERENCES config.rule_circ_duration (id) DEFERRABLE INITIALLY DEFERRED,
-    recurring_fine_rule  INT     NOT NULL REFERENCES config.rule_recurring_fine (id) DEFERRABLE INITIALLY DEFERRED,
-    max_fine_rule        INT     NOT NULL REFERENCES config.rule_max_fine (id) DEFERRABLE INITIALLY DEFERRED,
+    circulate            BOOL    DEFAULT TRUE,    -- Hard "can't circ" flag requiring an override
+    duration_rule        INT     REFERENCES config.rule_circ_duration (id) DEFERRABLE INITIALLY DEFERRED,
+    recurring_fine_rule  INT     REFERENCES config.rule_recurring_fine (id) DEFERRABLE INITIALLY DEFERRED,
+    max_fine_rule        INT     REFERENCES config.rule_max_fine (id) DEFERRABLE INITIALLY DEFERRED,
     script_test          TEXT,                           -- javascript source 
     total_copy_hold_ratio     FLOAT,
     available_copy_hold_ratio FLOAT,
@@ -135,7 +135,7 @@
     CONSTRAINT cm_once_per_test UNIQUE (circ_mod_test, circ_mod)
 );
 
-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$
+CREATE OR REPLACE FUNCTION action.find_circ_matrix_matchpoint( context_ou INT, match_item BIGINT, match_user INT, renewal BOOL, debug_rows BOOL DEFAULT false ) RETURNS SETOF config.circ_matrix_matchpoint AS $func$
 DECLARE
     current_group    permission.grp_tree%ROWTYPE;
     user_object    actor.usr%ROWTYPE;
@@ -144,6 +144,8 @@
     rec_descriptor    metabib.rec_descriptor%ROWTYPE;
     current_mp    config.circ_matrix_matchpoint%ROWTYPE;
     matchpoint    config.circ_matrix_matchpoint%ROWTYPE;
+    row_list    integer[];
+    row_pos    integer;
 BEGIN
     SELECT INTO user_object * FROM actor.usr WHERE id = match_user;
     SELECT INTO item_object * FROM asset.copy WHERE id = match_item;
@@ -217,22 +219,49 @@
                 CONTINUE WHEN user_object.dob IS NULL OR current_mp.usr_age_upper_bound > age(user_object.dob);
             END IF;
 
-
             -- everything was undefined or matched
-            matchpoint = current_mp;
+            row_list = row_list || current_mp.id;
+            IF matchpoint.id IS NULL THEN
+                matchpoint = current_mp;
+            ELSE
+                IF matchpoint.circulate IS NULL THEN
+                    matchpoint.circulate = current_mp.circulate;
+                END IF;
+                IF matchpoint.duration_rule IS NULL THEN
+                    matchpoint.duration_rule = current_mp.duration_rule;
+                END IF;
+                IF matchpoint.recurring_fine_rule IS NULL THEN
+                    matchpoint.recurring_fine_rule = current_mp.recurring_fine_rule;
+                END IF;
+                IF matchpoint.max_fine_rule IS NULL THEN
+                    matchpoint.max_fine_rule = current_mp.max_fine_rule;
+                END IF;
+            END IF;
 
-            EXIT WHEN matchpoint.id IS NOT NULL;
+            EXIT WHEN matchpoint.circulate IS NOT NULL AND matchpoint.duration_rule IS NOT NULL AND matchpoint.recurring_fine_rule IS NOT NULL AND matchpoint.max_fine_rule IS NOT NULL;
         END LOOP;
 
-        EXIT WHEN current_group.parent IS NULL OR matchpoint.id IS NOT NULL;
+        EXIT WHEN current_group.parent IS NULL OR ( matchpoint.circulate IS NOT NULL AND matchpoint.duration_rule IS NOT NULL AND matchpoint.recurring_fine_rule IS NOT NULL AND matchpoint.max_fine_rule IS NOT NULL );
 
         SELECT INTO current_group * FROM permission.grp_tree WHERE id = current_group.parent;
     END LOOP;
 
-    RETURN matchpoint;
+    IF debug_rows THEN
+        matchpoint.id = -1;
+    END IF;
+    RETURN NEXT matchpoint;
+    IF debug_rows THEN
+        SELECT INTO row_pos array_lower(row_list,1);
+        WHILE row_pos <= array_upper(row_list,1) LOOP
+            RETURN QUERY SELECT * FROM config.circ_matrix_matchpoint WHERE id=row_list[row_pos];
+        END LOOP;
+    END IF;
 END;
 $func$ LANGUAGE plpgsql;
 
+-- Remove previous version of the function if it is there (lacking optional argument)
+DROP FUNCTION IF EXISTS action.find_circ_matrix_matchpoint( context_ou INT, match_item BIGINT, match_user INT, renewal BOOL );
+
 CREATE TYPE action.hold_stats AS (
     hold_count              INT,
     copy_count              INT,
Developer's Certificate of Origin 1.1

By making a contribution to this project, I certify that:

(a) The contribution was created in whole or in part by me and I
    have the right to submit it under the open source license
    indicated in the file; or

(b) The contribution is based upon previous work that, to the best
    of my knowledge, is covered under an appropriate open source
    license and I have the right under that license to submit that
    work with modifications, whether created in whole or in part
    by me, under the same open source license (unless I am
    permitted to submit under a different license), as indicated
    in the file; or

(c) The contribution was provided directly to me by some other
    person who certified (a), (b) or (c) and I have not modified
    it.

(d) I understand and agree that this project and the contribution
    are public and that a record of the contribution (including all
    personal information I submit with it, including my sign-off) is
    maintained indefinitely and may be redistributed consistent with
    this project or the open source license(s) involved.

Signed-off-by: Thomas Berezansky <[email protected]>

Reply via email to