]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/sql/Pg/400.schema.action_trigger.sql
lp1777677 Test Notification Method
[Evergreen.git] / Open-ILS / src / sql / Pg / 400.schema.action_trigger.sql
1 /*
2  * Copyright (C) 2009  Equinox Software, Inc.
3  * Mike Rylander <miker@esilibrary.com> 
4  *
5  * This program is free software; you can redistribute it and/or
6  * modify it under the terms of the GNU General Public License
7  * as published by the Free Software Foundation; either version 2
8  * of the License, or (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  */
16
17 DROP SCHEMA IF EXISTS action_trigger CASCADE;
18
19 BEGIN;
20
21 CREATE SCHEMA action_trigger;
22
23 CREATE TABLE action_trigger.hook (
24     key         TEXT    PRIMARY KEY,
25     core_type   TEXT    NOT NULL,
26     description TEXT,
27     passive     BOOL    NOT NULL DEFAULT FALSE
28 );
29 INSERT INTO action_trigger.hook (key,core_type,description) VALUES ('checkout','circ','Item checked out to user');
30 INSERT INTO action_trigger.hook (key,core_type,description) VALUES ('checkin','circ','Item checked in');
31 INSERT INTO action_trigger.hook (key,core_type,description) VALUES ('lost','circ','Circulating Item marked Lost');
32 INSERT INTO action_trigger.hook (key,core_type,description) VALUES ('lost.found','circ','Lost Circulating Item checked in');
33 INSERT INTO action_trigger.hook (key,core_type,description) VALUES ('lost.auto','circ','Circulating Item automatically marked lost');
34 INSERT INTO action_trigger.hook (key,core_type,description) VALUES ('claims_returned','circ','Circulating Item marked Claims Returned');
35 INSERT INTO action_trigger.hook (key,core_type,description) VALUES ('claims_returned.found','circ','Claims Returned Circulating Item is checked in');
36 INSERT INTO action_trigger.hook (key,core_type,description) VALUES ('missing','acp','Item marked Missing');
37 INSERT INTO action_trigger.hook (key,core_type,description) VALUES ('missing.found','acp','Missing Item checked in');
38 INSERT INTO action_trigger.hook (key,core_type,description) VALUES ('transit.start','acp','An Item is placed into transit');
39 INSERT INTO action_trigger.hook (key,core_type,description) VALUES ('transit.finish','acp','An Item is received from a transit');
40 INSERT INTO action_trigger.hook (key,core_type,description) VALUES ('hold_request.success','ahr','A hold is successfully placed');
41 INSERT INTO action_trigger.hook (key,core_type,description) VALUES ('hold_request.failure','ahr','A hold is attempted but not successfully placed');
42 INSERT INTO action_trigger.hook (key,core_type,description) VALUES ('hold.capture','ahr','A targeted Item is captured for a hold');
43 INSERT INTO action_trigger.hook (key,core_type,description) VALUES ('hold.available','ahr','A held item is ready for pickup');
44 INSERT INTO action_trigger.hook (key,core_type,description) VALUES ('hold_transit.start','ahtc','A hold-captured Item is placed into transit');
45 INSERT INTO action_trigger.hook (key,core_type,description) VALUES ('hold_transit.finish','ahtc','A hold-captured Item is received from a transit');
46 INSERT INTO action_trigger.hook (key,core_type,description,passive) VALUES ('checkout.due','circ','Checked out Item is Due',TRUE);
47 INSERT INTO action_trigger.hook (key,core_type,description,passive) VALUES ('penalty.PATRON_EXCEEDS_FINES','ausp','Patron has exceeded allowed fines',TRUE);
48 INSERT INTO action_trigger.hook (key,core_type,description,passive) VALUES ('penalty.PATRON_EXCEEDS_OVERDUE_COUNT','ausp','Patron has exceeded allowed overdue count',TRUE);
49 INSERT INTO action_trigger.hook (key,core_type,description,passive) VALUES ('penalty.PATRON_EXCEEDS_CHECKOUT_COUNT','ausp','Patron has exceeded allowed checkout count',TRUE);
50 INSERT INTO action_trigger.hook (key,core_type,description,passive) VALUES ('penalty.PATRON_EXCEEDS_COLLECTIONS_WARNING','ausp','Patron has exceeded maximum fine amount for collections department warning',TRUE);
51 INSERT INTO action_trigger.hook (key,core_type,description,passive) VALUES ('acqpo.activated','acqpo','Purchase order was activated',FALSE);
52 INSERT INTO action_trigger.hook (key,core_type,description) VALUES ('format.po.html','acqpo','Formats a Purchase Order as an HTML document');
53 INSERT INTO action_trigger.hook (key,core_type,description) VALUES ('format.po.pdf','acqpo','Formats a Purchase Order as a PDF document');
54 INSERT INTO action_trigger.hook (key,core_type,description) VALUES ('damaged','acp','Item marked damaged');
55 INSERT INTO action_trigger.hook (key,core_type,description) VALUES ('checkout.damaged','circ','A circulating item is marked damaged and the patron is fined');
56 INSERT INTO action_trigger.hook (key,core_type,description) VALUES ('renewal','circ','Item renewed to user');
57 INSERT INTO action_trigger.hook (key,core_type,description) VALUES ('checkout.due.emergency_closing','aecc','Circulation due date was adjusted by the Emergency Closing handler');
58 INSERT INTO action_trigger.hook (key,core_type,description) VALUES ('hold.shelf_expire.emergency_closing','aech','Hold shelf expire time was adjusted by the Emergency Closing handler');
59 INSERT INTO action_trigger.hook (key,core_type,description) VALUES ('booking.due.emergency_closing','aecr','Booking reservation return date was adjusted by the Emergency Closing handler');
60 INSERT INTO action_trigger.hook (key,core_type,description) VALUES ('bre.edit','bre','A bib record was edited');
61 INSERT INTO action_trigger.hook (key, core_type, description) VALUES ('au.email.test', 'au', 'A test email has been requested for this user');
62 INSERT INTO action_trigger.hook (key, core_type, description) VALUES ('au.sms_text.test', 'au', 'A test SMS has been requested for this user');
63
64 -- and much more, I'm sure
65
66 -- Specialized collection modules.  Given an FM object, gather some info and return a scalar or ref.
67 CREATE TABLE action_trigger.collector (
68     module      TEXT    PRIMARY KEY, -- All live under the OpenILS::Trigger::Collector:: namespace
69     description TEXT    
70 );
71 INSERT INTO action_trigger.collector (module,description) VALUES ('fourty_two','Returns the answer to life, the universe and everything');
72 --INSERT INTO action_trigger.collector (module,description) VALUES ('CircCountsByCircMod','Count of Circulations for a User, broken down by circulation modifier');
73
74 -- Simple tests on an FM object from hook.core_type to test for "should we still do this."
75 CREATE TABLE action_trigger.validator (
76     module      TEXT    PRIMARY KEY, -- All live under the OpenILS::Trigger::Validator:: namespace
77     description TEXT    
78 );
79 INSERT INTO action_trigger.validator (module,description) VALUES ('fourty_two','Returns the answer to life, the universe and everything');
80 INSERT INTO action_trigger.validator (module,description) VALUES ('NOOP_True','Always returns true -- validation always passes');
81 INSERT INTO action_trigger.validator (module,description) VALUES ('NOOP_False','Always returns false -- validation always fails');
82 INSERT INTO action_trigger.validator (module,description) VALUES ('CircIsOpen','Check that the circulation is still open');
83 INSERT INTO action_trigger.validator (module,description) VALUES ('HoldIsAvailable','Check that an item is on the hold shelf');
84 INSERT INTO action_trigger.validator (module,description) VALUES ('CircIsOverdue','Check that the circulation is overdue');
85 INSERT INTO action_trigger.validator (module,description) VALUES ('MaxPassiveDelayAge','Check that the event is not too far past the delay_field time -- requires a max_delay_age interval parameter');
86 INSERT INTO action_trigger.validator (module,description) VALUES ('MinPassiveTargetAge','Check that the target is old enough to be used by this event -- requires a min_target_age interval parameter, and accepts an optional target_age_field to specify what time to use for offsetting');
87
88 -- After an event passes validation (action_trigger.validator), the reactor processes it.
89 CREATE TABLE action_trigger.reactor (
90     module      TEXT    PRIMARY KEY, -- All live under the OpenILS::Trigger::Reactor:: namespace
91     description TEXT    
92 );
93
94 INSERT INTO action_trigger.reactor (module,description) VALUES
95 (   'fourty_two',
96     oils_i18n_gettext(
97         'fourty_two',
98         'Returns the answer to life, the universe and everything',
99         'atreact',
100         'description'
101     )
102 );
103 INSERT INTO action_trigger.reactor (module,description) VALUES
104 (   'NOOP_True',
105     oils_i18n_gettext(
106         'NOOP_True',
107         'Always returns true -- reaction always passes',
108         'atreact',
109         'description'
110     )
111 );
112 INSERT INTO action_trigger.reactor (module,description) VALUES
113 (   'NOOP_False',
114     oils_i18n_gettext(
115         'NOOP_False',
116         'Always returns false -- reaction always fails',
117         'atreact',
118         'description'
119     )
120 );
121 INSERT INTO action_trigger.reactor (module,description) VALUES
122 (   'SendEmail',
123     oils_i18n_gettext(
124         'SendEmail',
125         'Send an email based on a user-defined template',
126         'atreact',
127         'description'
128     )
129 );
130
131 -- TODO: build a PDF generator
132 --INSERT INTO action_trigger.reactor (module,description) VALUES
133 --(   'GenerateBatchOverduePDF',
134 --    oils_i18n_gettext(
135 --        'GenerateBatchOverduePDF',
136 --        'Output a batch PDF of overdue notices for printing',
137 --        'atreact',
138 --        'description'
139 --    )
140 --);
141
142 INSERT INTO action_trigger.reactor (module,description) VALUES
143 (   'MarkItemLost',
144     oils_i18n_gettext(
145         'MarkItemLost',
146         'Marks a circulation and associated item as lost',
147         'atreact',
148         'description'
149     )
150 );
151 INSERT INTO action_trigger.reactor (module,description) VALUES
152 (   'ApplyCircFee',
153     oils_i18n_gettext(
154         'ApplyCircFee',
155         'Applies a billing with a pre-defined amount to a circulation',
156         'atreact',
157         'description'
158     )
159 );
160 INSERT INTO action_trigger.reactor (module,description) VALUES
161 (   'ProcessTemplate',
162     oils_i18n_gettext(
163         'ProcessTemplate',
164         'Processes the configured template',
165         'atreact',
166         'description'
167     )
168 );
169
170 -- After an event is reacted to (either success or failure) a cleanup module is run against the resulting environment
171 CREATE TABLE action_trigger.cleanup (
172     module      TEXT    PRIMARY KEY, -- All live under the OpenILS::Trigger::Cleanup:: namespace
173     description TEXT    
174 );
175 INSERT INTO action_trigger.cleanup (module,description) VALUES ('fourty_two','Returns the answer to life, the universe and everything');
176 INSERT INTO action_trigger.cleanup (module,description) VALUES ('NOOP_True','Always returns true -- cleanup always passes');
177 INSERT INTO action_trigger.cleanup (module,description) VALUES ('NOOP_False','Always returns false -- cleanup always fails');
178 INSERT INTO action_trigger.cleanup (module,description) VALUES ('ClearAllPending','Remove all future, pending notifications for this target');
179
180 CREATE TABLE action_trigger.event_definition (
181     id              SERIAL      PRIMARY KEY,
182     active          BOOL        NOT NULL DEFAULT TRUE,
183     owner           INT         NOT NULL REFERENCES actor.org_unit (id) DEFERRABLE INITIALLY DEFERRED,
184     name            TEXT        NOT NULL,
185     hook            TEXT        NOT NULL REFERENCES action_trigger.hook (key) DEFERRABLE INITIALLY DEFERRED,
186     validator       TEXT        NOT NULL REFERENCES action_trigger.validator (module) DEFERRABLE INITIALLY DEFERRED,
187     reactor         TEXT        NOT NULL REFERENCES action_trigger.reactor (module) DEFERRABLE INITIALLY DEFERRED,
188     cleanup_success TEXT        REFERENCES action_trigger.cleanup (module) DEFERRABLE INITIALLY DEFERRED,
189     cleanup_failure TEXT        REFERENCES action_trigger.cleanup (module) DEFERRABLE INITIALLY DEFERRED,
190     delay           INTERVAL    NOT NULL DEFAULT '5 minutes',
191     max_delay       INTERVAL,
192     repeat_delay    INTERVAL,
193     usr_field       TEXT,
194     opt_in_setting  TEXT        REFERENCES config.usr_setting_type (name) DEFERRABLE INITIALLY DEFERRED,
195     delay_field     TEXT,                 -- for instance, xact_start on a circ hook ... look for fields on hook.core_type where datatype=timestamp? If not set, delay from now()
196     group_field     TEXT,                 -- field from this.hook.core_type to batch event targets together on, fed into reactor a group at a time.
197     template        TEXT,                 -- the TT block.  will have an 'environment' hash (or array of hashes, grouped events) built up by validator and collector(s), which can be modified.
198     granularity     TEXT,   -- could specify a batch which is the only time these events should actually run
199
200     message_template        TEXT,
201     message_usr_path        TEXT,
202     message_library_path    TEXT,
203     message_title           TEXT,
204     retention_interval      INTERVAL,
205
206     CONSTRAINT ev_def_owner_hook_val_react_clean_delay_once UNIQUE (owner, hook, validator, reactor, delay, delay_field),
207     CONSTRAINT ev_def_name_owner_once UNIQUE (owner, name)
208 );
209
210 CREATE OR REPLACE FUNCTION action_trigger.check_valid_retention_interval() 
211     RETURNS TRIGGER AS $_$
212 BEGIN
213     /*
214      * 1. Retention intervals are always allowed on active hooks.
215      * 2. On passive hooks, retention intervals are only allowed
216      *    when the event definition has a max_delay value and the
217      *    retention_interval value is greater than the difference 
218      *    beteween the delay and max_delay values.
219      */ 
220     PERFORM TRUE FROM action_trigger.hook 
221         WHERE key = NEW.hook AND NOT passive;
222
223     IF FOUND THEN
224         RETURN NEW;
225     END IF;
226
227     IF NEW.max_delay IS NOT NULL THEN
228         IF EXTRACT(EPOCH FROM NEW.retention_interval) > 
229             ABS(EXTRACT(EPOCH FROM (NEW.max_delay - NEW.delay))) THEN
230             RETURN NEW; -- all good
231         ELSE
232             RAISE EXCEPTION 'retention_interval is too short';
233         END IF;
234     ELSE
235         RAISE EXCEPTION 'retention_interval requires max_delay';
236     END IF;
237 END;
238 $_$ LANGUAGE PLPGSQL;
239
240 CREATE TRIGGER is_valid_retention_interval 
241     BEFORE INSERT OR UPDATE ON action_trigger.event_definition
242     FOR EACH ROW WHEN (NEW.retention_interval IS NOT NULL)
243     EXECUTE PROCEDURE action_trigger.check_valid_retention_interval();
244
245 CREATE TABLE action_trigger.environment (
246     id          SERIAL  PRIMARY KEY,
247     event_def   INT     NOT NULL REFERENCES action_trigger.event_definition (id) DEFERRABLE INITIALLY DEFERRED,
248     path        TEXT,       -- fields to flesh. given a hook with a core_type of circ, imagine circ_lib.parent_ou expanding to
249                             -- {flesh: 2, flesh_fields: {circ: ['circ_lib'], aou: ['parent_ou']}} ... default is to flesh all
250                             -- at flesh depth 1
251     collector   TEXT    REFERENCES action_trigger.collector (module) DEFERRABLE INITIALLY DEFERRED, -- if set, given the object at 'path', return some data
252                                                                       -- to be stashed at environment.<label>
253     label       TEXT    CHECK (label NOT IN ('result','target','event')),
254     CONSTRAINT env_event_label_once UNIQUE (event_def,label)
255 );
256
257 CREATE TABLE action_trigger.event_output (
258     id              BIGSERIAL   PRIMARY KEY,
259     create_time     TIMESTAMPTZ NOT NULL DEFAULT NOW(),
260     is_error        BOOLEAN     NOT NULL DEFAULT FALSE,
261     data            TEXT        NOT NULL
262 );
263
264 CREATE TABLE action_trigger.event (
265     id              BIGSERIAL   PRIMARY KEY,
266     target          BIGINT      NOT NULL, -- points at the id from class defined by event_def.hook.core_type
267     event_def       INT         REFERENCES action_trigger.event_definition (id) DEFERRABLE INITIALLY DEFERRED,
268     add_time        TIMESTAMPTZ NOT NULL DEFAULT NOW(),
269     run_time        TIMESTAMPTZ NOT NULL,
270     start_time      TIMESTAMPTZ,
271     update_time     TIMESTAMPTZ,
272     complete_time   TIMESTAMPTZ,
273     update_process  INT,
274     state           TEXT        NOT NULL DEFAULT 'pending' CHECK (state IN ('pending','invalid','found','collecting','collected','validating','valid','reacting','reacted','cleaning','complete','error')),
275     user_data       TEXT        CHECK (user_data IS NULL OR is_json( user_data )),
276     template_output BIGINT      REFERENCES action_trigger.event_output (id),
277     error_output    BIGINT      REFERENCES action_trigger.event_output (id),
278     async_output    BIGINT      REFERENCES action_trigger.event_output (id)
279 );
280 CREATE INDEX atev_target_def_idx ON action_trigger.event (target,event_def);
281 CREATE INDEX atev_def_state ON action_trigger.event (event_def,state);
282 CREATE INDEX atev_template_output ON action_trigger.event (template_output);
283 CREATE INDEX atev_async_output ON action_trigger.event (async_output);
284 CREATE INDEX atev_error_output ON action_trigger.event (error_output);
285
286 CREATE TABLE action_trigger.event_params (
287     id          BIGSERIAL   PRIMARY KEY,
288     event_def   INT         NOT NULL REFERENCES action_trigger.event_definition (id) DEFERRABLE INITIALLY DEFERRED,
289     param       TEXT        NOT NULL, -- the key under environment.event.params to store the output of ...
290     value       TEXT        NOT NULL, -- ... the eval() output of this.  Has access to environment (and, well, all of perl)
291     CONSTRAINT event_params_event_def_param_once UNIQUE (event_def,param)
292 );
293
294 CREATE OR REPLACE FUNCTION action_trigger.purge_events() RETURNS VOID AS $_$
295 /**
296   * Deleting expired events without simultaneously deleting their outputs
297   * creates orphaned outputs.  Deleting their outputs and all of the events 
298   * linking back to them, plus any outputs those events link to is messy and 
299   * inefficient.  It's simpler to handle them in 2 sweeping steps.
300   *
301   * 1. Delete expired events.
302   * 2. Delete orphaned event outputs.
303   *
304   * This has the added benefit of removing outputs that may have been
305   * orphaned by some other process.  Such outputs are not usuable by
306   * the system.
307   *
308   * This does not guarantee that all events within an event group are
309   * purged at the same time.  In such cases, the remaining events will
310   * be purged with the next instance of the purge (or soon thereafter).
311   * This is another nod toward efficiency over completeness of old 
312   * data that's circling the bit bucket anyway.
313   */
314 BEGIN
315
316     DELETE FROM action_trigger.event WHERE id IN (
317         SELECT evt.id
318         FROM action_trigger.event evt
319         JOIN action_trigger.event_definition def ON (def.id = evt.event_def)
320         WHERE def.retention_interval IS NOT NULL 
321             AND evt.state <> 'pending'
322             AND evt.update_time < (NOW() - def.retention_interval)
323     );
324
325     WITH linked_outputs AS (
326         SELECT templates.id AS id FROM (
327             SELECT DISTINCT(template_output) AS id
328                 FROM action_trigger.event WHERE template_output IS NOT NULL
329             UNION
330             SELECT DISTINCT(error_output) AS id
331                 FROM action_trigger.event WHERE error_output IS NOT NULL
332             UNION
333             SELECT DISTINCT(async_output) AS id
334                 FROM action_trigger.event WHERE async_output IS NOT NULL
335         ) templates
336     ) DELETE FROM action_trigger.event_output
337         WHERE id NOT IN (SELECT id FROM linked_outputs);
338
339 END;
340 $_$ LANGUAGE PLPGSQL;
341
342 COMMIT;
343