]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/Application/EbookAPI/Test.pm
adab52c09426f339d5d2cc10c38970e85a255cc6
[working/Evergreen.git] / Open-ILS / src / perlmods / lib / OpenILS / Application / EbookAPI / Test.pm
1 #!/usr/bin/perl
2
3 # Copyright (C) 2015 BC Libraries Cooperative
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 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software
17 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
18
19 # ====================================================================== 
20 # OpenSRF requests are handled by the main OpenILS::Application::EbookAPI module,
21 # which determines which "handler" submodule to use based on the params of the
22 # OpenSRF request.  Each vendor API (OneClickdigital, OverDrive, etc.) has its
23 # own separate handler class, since they all work a little differently.
24 #
25 # An instance of the handler class represents an EbookAPI session -- that is, we
26 # instantiate a new handler object when we start a new session with the external API.
27 # Thus everything we need to talk to the API, like client keys or auth tokens, is
28 # an attribute of the handler object.
29 #
30 # API endpoints are defined in the handler class.  The handler constructs HTTP
31 # requests, then passes them to the the request() method of the parent class
32 # (OpenILS::Application::EbookAPI), which sets some default headers and manages
33 # the actual mechanics of sending the request and receiving the response.  It's
34 # up to the handler class to do something with the response.
35 #
36 # At a minimum, each handler must have the following methods, since the parent
37 # class presumes they exist; it may be a no-op if the API doesn't support that
38 # bit of functionality:
39 #
40 #   - initialize: assign values for basic attributes (e.g. library_id,
41 #     basic_token) based on library settings
42 #   - do_client_auth: authenticate client with external API (e.g. get client
43 #     token if needed)
44 #   - do_patron_auth: get a patron-specific bearer token, or just the patron ID
45 #   - get_title_info: get basic title details (title, author, optional cover image)
46 #   - do_holdings_lookup: how many total/available "copies" are there for this
47 #     title? (n/a for OneClickdigital)
48 #   - do_availability_lookup: does this title have available "copies"? y/n
49 #   - checkout
50 #   - renew
51 #   - checkin
52 #   - place_hold
53 #   - suspend_hold (n/a for OneClickdigital)
54 #   - cancel_hold
55 #   - get_patron_checkouts: returns an array of hashrefs representing checkouts;
56 #     each checkout hashref has the following keys:
57 #       - xact_id
58 #       - title_id
59 #       - due_date
60 #       - download_url
61 #       - title
62 #       - author
63 #   - get_patron_holds
64 # ====================================================================== 
65
66 package OpenILS::Application::EbookAPI::Test;
67
68 use strict;
69 use warnings;
70
71 use OpenILS::Application;
72 use OpenILS::Application::EbookAPI;
73 use base qw/OpenILS::Application::EbookAPI/;
74 use OpenSRF::AppSession;
75 use OpenSRF::EX qw(:try);
76 use OpenSRF::Utils::SettingsClient;
77 use OpenSRF::Utils::Logger qw($logger);
78 use OpenSRF::Utils::Cache;
79 use OpenILS::Application::AppUtils;
80 use DateTime;
81 use DateTime::Format::ISO8601;
82
83 my $U = 'OpenILS::Application::AppUtils';
84
85 # create new handler object
86 sub new {
87     my( $class, $args ) = @_;
88
89     # A new handler object represents a new API session, so we instantiate it
90     # by passing it a hashref containing the following basic attributes
91     # available to us when we start the session:
92     #   - vendor: a string indicating the vendor whose API we're talking to
93     #   - ou: org unit ID for current session
94     #   - session_id: unique ID for the session represented by this object
95
96     $class = ref $class || $class;
97     return bless $args, $class;
98 }
99
100 # set API-specific handler attributes based on library settings
101 sub initialize {
102     my $self = shift;
103
104     # At a minimum, you are likely to need some kind of basic API key or token
105     # to allow the client (Evergreen) to use the API.
106     # Other attributes will vary depending on the API.  Consult your API
107     # documentation for details.
108
109     return $self;
110 }
111
112 # authorize client session against API
113 sub do_client_auth {
114     my $self = shift;
115
116     # Some APIs require client authorization, and may return an auth token
117     # which must be included in subsequent requests.  This is where you do
118     # that.  If you get an auth token, you'll want to add it as an attribute to
119     # the handler object so that it's available to use in subsequent requests.
120     # If your API doesn't require this step, you don't need to return anything
121     # here.
122
123     return;
124 }
125
126 # authenticate patron against API
127 sub do_patron_auth {
128     my $self = shift;
129
130     # We authenticate the patron using the barcode of their active card.
131     # We may capture this on OPAC login (along with password, if required),
132     # in which case it should already be an attribute of the handler object;
133     # otherwise, it should be passed to this method as a parameter.
134     my $barcode = shift;
135     if ($barcode) {
136         if (!$self->{patron_barcode}) {
137             $self->{patron_barcode} = $barcode;
138         } elsif ($barcode ne $self->{patron_barcode}) {
139             $logger->error("EbookAPI: patron barcode in auth request does not match patron barcode for this session");
140             return;
141         }
142     } else {
143         if (!$self->{patron_barcode}) {
144             $logger->error("EbookAPI: Cannot authenticate patron with unknown barcode");
145         } else {
146             $barcode = $self->{patron_barcode};
147         }
148     }
149
150     # We really don't want to be handling the patron's unencrypted password.
151     # But if we need to, it should be added to our handler object on login
152     # via the open-ils.ebook_api.patron.cache_password OpenSRF API call
153     # before we attempt to authenticate the patron against the external API.
154     my $password;
155     if ($self->{patron_password}) {
156         $password = $self->{patron_password};
157     }
158
159     # return external patron ID or patron auth token
160
161     # For testing, only barcode 99999359616 is valid.
162     return 'USER001' if ($barcode eq '99999359616');
163
164     # All other values return undef.
165     return undef;
166 }
167
168 # get basic info (title, author, eventually a thumbnail URL) for a title
169 sub get_title_info {
170     my $self = shift;
171
172     # External ID for title.  Depending on the API, this could be an ISBN
173     # or an identifier unique to that vendor.
174     my $title_id = shift;
175
176     # Prepare data structure to be used as return value.
177     my $title_info = {
178         title  => '',
179         author => ''
180     };
181
182     # If title lookup fails or title is not found, our return value
183     # is somewhat different.
184     my $title_not_found = {
185         error => 'Title not found.'
186     };
187
188     # For testing purposes, we have only three valid titles (001, 002, 003).
189     # All other title IDs return an error message.
190     if ($title_id eq '001') {
191         $title_info->{title} = 'The Fellowship of the Ring';
192         $title_info->{author} = 'J.R.R. Tolkien';
193     } elsif ($title_id eq '002') {
194         $title_info->{title} = 'The Two Towers';
195         $title_info->{author} = 'J.R.R. Tolkien';
196     } elsif ($title_id eq '003') {
197         $title_info->{title} = 'The Return of the King';
198         $title_info->{author} = 'J.R.R. Tolkien';
199     } else {
200         return $title_not_found;
201     }
202     return $title_info;
203 }
204
205 # get detailed holdings information (copy counts and formats), OR basic
206 # availability if detailed info is not provided by the API
207 sub do_holdings_lookup {
208     my $self = shift;
209
210     # External ID for title.  Depending on the API, this could be an ISBN
211     # or an identifier unique to that vendor.
212     my $title_id = shift;
213
214     # Prepare data structure to be used as return value.
215     # NOTE: If the external API does not provide detailed holdings info,
216     # return simple availability information: { available => 1 }
217     my $holdings = {
218         copies_owned => 0,
219         copies_available => 0,
220         formats => []
221     };
222
223     # 001 and 002 are unavailable.
224     if ($title_id eq '001' || $title_id eq '002') {
225         $holdings->{copies_owned} = 1;
226         $holdings->{copies_available} = 0;
227         push @{$holdings->{formats}}, 'ebook';
228     }
229
230     # 003 is available.
231     if ($title_id eq '003') {
232         $holdings->{copies_owned} = 1;
233         $holdings->{copies_available} = 1;
234         push @{$holdings->{formats}}, 'ebook';
235     }
236
237     # All other title IDs are unknown.
238
239     return $holdings;
240 }
241
242 # look up whether a title is currently available for checkout; returns a boolean value
243 sub do_availability_lookup {
244     my $self = shift;
245
246     # External ID for title.  Depending on the API, this could be an ISBN
247     # or an identifier unique to that vendor.
248     my $title_id = shift;
249
250     # At this point, you would lookup title availability via an API request.
251     # In our case, since this is a test module, we just return availability info
252     # based on hard-coded values:
253
254     # 001 and 002 are unavailable.
255     return 0 if ($title_id eq '001');
256     return 0 if ($title_id eq '002');
257
258     # 003 is available.
259     return 1 if ($title_id eq '003');
260
261     # All other title IDs are unknown.
262     return undef;
263 }
264
265 # check out a title to a patron
266 sub checkout {
267     my $self = shift;
268
269     # External ID of title to be checked out.
270     my $title_id = shift;
271
272     # Patron ID or patron auth token, as returned by do_patron_auth().
273     my $user_token = shift;
274
275     # If checkout succeeds, the response is a hashref with the following fields:
276     # - due_date
277     # - xact_id (optional)
278     #
279     # If checkout fails, the response is a hashref with the following fields:
280     # - error_msg: a string containing an error message or description of why
281     #   the checkout failed (e.g. "Checkout limit of (4) reached").
282     #
283     # If no valid response is received from the API, return undef.
284
285     # For testing purposes, user ID USER001 is our only valid user, 
286     # and title 003 is the only available title.
287     if ($title_id && $user_token) {
288         if ($user_token eq 'USER001' && $title_id eq '003') {
289             return { due_date => DateTime->today()->add( days => 14 )->iso8601() };
290         } else {
291             return { msg => 'Checkout failed.' };
292         }
293     } else {
294         return undef;
295     }
296
297 }
298
299 sub renew {
300     my $self = shift;
301
302     # External ID of title to be renewed.
303     my $title_id = shift;
304
305     # Patron ID or patron auth token, as returned by do_patron_auth().
306     my $user_token = shift;
307
308     # If renewal succeeds, the response is a hashref with the following fields:
309     # - due_date
310     # - xact_id (optional)
311     #
312     # If renewal fails, the response is a hashref with the following fields:
313     # - error_msg: a string containing an error message or description of why
314     #   the renewal failed (e.g. "Renewal limit reached").
315     #
316     # If no valid response is received from the API, return undef.
317
318     # For testing purposes, user ID USER001 is our only valid user, 
319     # and title 001 is the only renewable title.
320     if ($title_id && $user_token) {
321         if ($user_token eq 'USER001' && $title_id eq '001') {
322             return { due_date => DateTime->today()->add( days => 14 )->iso8601() };
323         } else {
324             return { error_msg => 'Renewal failed.' };
325         }
326     } else {
327         return undef;
328     }
329 }
330
331 sub checkin {
332     my $self = shift;
333
334     # External ID of title to be checked in.
335     my $title_id = shift;
336
337     # Patron ID or patron auth token, as returned by do_patron_auth().
338     my $user_token = shift;
339
340     # If checkin succeeds, return an empty hashref (actually it doesn't
341     # need to be empty, it just must NOT contain "error_msg" as a key).
342     #
343     # If checkin fails, return a hashref with the following fields:
344     # - error_msg: a string containing an error message or description of why
345     #   the checkin failed (e.g. "Checkin failed").
346     #
347     # If no valid response is received from the API, return undef.
348
349     # For testing purposes, user ID USER001 is our only valid user, 
350     # and title 003 is the only title that can be checked in.
351     if ($title_id && $user_token) {
352         if ($user_token eq 'USER001' && $title_id eq '003') {
353             return {};
354         } else {
355             return { error_msg => 'Checkin failed' };
356         }
357     } else {
358         return undef;
359     }
360 }
361
362 sub place_hold {
363     my $self = shift;
364
365     # External ID of title to be held.
366     my $title_id = shift;
367
368     # Patron ID or patron auth token, as returned by do_patron_auth().
369     my $user_token = shift;
370
371     # Email address of patron (optional, not used here).
372     my $email = shift;
373
374     # If hold is successfully placed, return a hashref with the following
375     # fields:
376     # - queue_position: this user's position in hold queue for this title
377     # - queue_size: total number of holds on this title
378     # - expire_date: when the hold expires
379     #
380     # If hold fails, return a hashref with the following fields:
381     # - error_msg: a string containing an error message or description of why
382     #   the hold failed (e.g. "Hold limit (4) reached").
383     #
384     # If no valid response is received from the API, return undef.
385
386     # For testing purposes, we always and only allow placing a hold on title
387     # 002 by user ID USER001.
388     if ($title_id && $user_token) {
389         if ($user_token eq 'USER001' && $title_id eq '002') {
390             return {
391                 queue_position => 1,
392                 queue_size => 1,
393                 expire_date => DateTime->today()->add( days => 70 )->iso8601()
394             };
395         } else {
396             return { error_msg => 'Unable to place hold' };
397         }
398     } else {
399         return undef;
400     }
401 }
402
403 sub cancel_hold {
404     my $self = shift;
405
406     # External ID of title.
407     my $title_id = shift;
408
409     # Patron ID or patron auth token, as returned by do_patron_auth().
410     my $user_token = shift;
411
412     # If hold is successfully canceled, return an empty hashref (actually it
413     # doesn't need to be empty, it just must NOT contain "error_msg" as a key).
414     #
415     # If hold is NOT canceled, return a hashref with the following fields:
416     # - error_msg: a string containing an error message or description of why
417     #   the hold was not canceled (e.g. "Hold could not be canceled"). 
418     #
419     # If no valid response is received from the API, return undef.
420
421     # For testing purposes, we always and only allow canceling a hold on title
422     # 002 by user ID USER001.
423     if ($title_id && $user_token) {
424         if ($user_token eq 'USER001' && $title_id eq '002') {
425             return {};
426         } else {
427             return { error_msg => 'Unable to cancel hold' };
428         }
429     } else {
430         return undef;
431     }
432 }
433
434 sub suspend_hold {
435 }
436
437 sub get_patron_checkouts {
438     my $self = shift;
439
440     # Patron ID or patron auth token.
441     my $user_token = shift;
442
443     # Return an array of hashrefs representing checkouts;
444     # each hashref should have the following keys:
445     #   - xact_id: unique ID for this transaction (if used by API)
446     #   - title_id: unique ID for this title
447     #   - due_date
448     #   - download_url
449     #   - title: title of item, formatted for display
450     #   - author: author of item, formatted for display
451
452     my $checkouts = [];
453     # USER001 is our only valid user, so we only return checkouts for them.
454     if ($user_token eq 'USER001') {
455         push @$checkouts, {
456             xact_id => '1',
457             title_id => '001',
458             due_date => DateTime->today()->add( days => 7 )->iso8601(),
459             download_url => 'http://example.com/ebookapi/t/001/download',
460             title => 'The Fellowship of the Ring',
461             author => 'J. R. R. Tolkien'
462         };
463     }
464     $self->{checkouts} = $checkouts;
465     return $self->{checkouts};
466 }
467
468 sub get_patron_holds {
469     my $self = shift;
470
471     # Patron ID or patron auth token.
472     my $user_token = shift;
473
474     # Return an array of hashrefs representing holds;
475     # each hashref should have the following keys:
476     #   - title_id: unique ID for this title
477     #   - queue_position: this user's position in hold queue for this title
478     #   - queue_size: total number of holds on this title
479     #   - is_ready: whether hold is currently available for checkout
480     #   - is_frozen: whether hold is suspended
481     #   - thaw_date: when hold suspension expires (if suspended)
482     #   - create_date: when the hold was placed
483     #   - expire_date: when the hold expires
484     #   - title: title of item, formatted for display
485     #   - author: author of item, formatted for display
486
487     my $holds = [];
488     # USER001 is our only valid user, so we only return checkouts for them.
489     if ($user_token eq 'USER001') {
490         push @$holds, {
491             title_id => '002',
492             queue_position => 1,
493             queue_size => 1,
494             is_ready => 0,
495             is_frozen => 0,
496             create_date => DateTime->today()->subtract( days => 10 )->iso8601(),
497             expire_date => DateTime->today()->add( days => 60 )->iso8601(),
498             title => 'The Two Towers',
499             author => 'J. R. R. Tolkien'
500         };
501     }
502     $self->{holds} = $holds;
503     return $self->{holds};
504 }
505