3 # Copyright (C) 2015 BC Libraries Cooperative
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.
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.
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.
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.
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.
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.
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:
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
44 # - do_patron_auth: get a patron-specific bearer token, or just the patron ID
45 # - do_holdings_lookup: how many total/available "copies" are there for this
46 # title? (n/a for OneClickdigital)
47 # - do_availability_lookup: does this title have available "copies"? y/n
52 # - suspend_hold (n/a for OneClickdigital)
54 # - get_patron_checkouts: returns an array of hashrefs representing checkouts;
55 # each checkout hashref has the following keys:
63 # ======================================================================
65 package OpenILS::Application::EbookAPI::Test;
70 use OpenILS::Application;
71 use OpenILS::Application::EbookAPI;
72 use base qw/OpenILS::Application::EbookAPI/;
73 use OpenSRF::AppSession;
74 use OpenSRF::EX qw(:try);
75 use OpenSRF::Utils::SettingsClient;
76 use OpenSRF::Utils::Logger qw($logger);
77 use OpenSRF::Utils::Cache;
78 use OpenILS::Application::AppUtils;
80 use DateTime::Format::ISO8601;
82 my $U = 'OpenILS::Application::AppUtils';
84 # create new handler object
86 my( $class, $args ) = @_;
88 # A new handler object represents a new API session, so we instantiate it
89 # by passing it a hashref containing the following basic attributes
90 # available to us when we start the session:
91 # - vendor: a string indicating the vendor whose API we're talking to
92 # - ou: org unit ID for current session
93 # - session_id: unique ID for the session represented by this object
95 $class = ref $class || $class;
96 return bless $args, $class;
99 # set API-specific handler attributes based on library settings
103 # At a minimum, you are likely to need some kind of basic API key or token
104 # to allow the client (Evergreen) to use the API.
105 # Other attributes will vary depending on the API. Consult your API
106 # documentation for details.
111 # authorize client session against API
115 # Some APIs require client authorization, and may return an auth token
116 # which must be included in subsequent requests. This is where you do
117 # that. If you get an auth token, you'll want to add it as an attribute to
118 # the handler object so that it's available to use in subsequent requests.
119 # If your API doesn't require this step, you don't need to return anything
125 # authenticate patron against API
129 # We authenticate the patron using the barcode of their active card.
130 # We may capture this on OPAC login (along with password, if required),
131 # in which case it should already be an attribute of the handler object;
132 # otherwise, it should be passed to this method as a parameter.
135 if (!$self->{patron_barcode}) {
136 $self->{patron_barcode} = $barcode;
137 } elsif ($barcode ne $self->{patron_barcode}) {
138 $logger->error("EbookAPI: patron barcode in auth request does not match patron barcode for this session");
142 if (!$self->{patron_barcode}) {
143 $logger->error("EbookAPI: Cannot authenticate patron with unknown barcode");
145 $barcode = $self->{patron_barcode};
149 # We really don't want to be handling the patron's unencrypted password.
150 # But if we need to, it should be added to our handler object on login
151 # via the open-ils.ebook_api.patron.cache_password OpenSRF API call
152 # before we attempt to authenticate the patron against the external API.
154 if ($self->{patron_password}) {
155 $password = $self->{patron_password};
158 # return external patron ID or patron auth token
160 # For testing, only barcode 99999359616 is valid.
161 return 'USER001' if ($barcode eq '99999359616');
163 # All other values return undef.
167 # get detailed holdings information (copy counts and formats), OR basic
168 # availability if detailed info is not provided by the API
169 sub do_holdings_lookup {
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;
176 # Prepare data structure to be used as return value.
177 # NOTE: If the external API does not provide detailed holdings info,
178 # return simple availability information: { available => 1 }
181 copies_available => 0,
185 # 001 and 002 are unavailable.
186 if ($title_id eq '001' || $title_id eq '002') {
187 $holdings->{copies_owned} = 1;
188 $holdings->{copies_available} = 0;
189 push @{$holdings->{formats}}, 'ebook';
193 if ($title_id eq '003') {
194 $holdings->{copies_owned} = 1;
195 $holdings->{copies_available} = 1;
196 push @{$holdings->{formats}}, 'ebook';
199 # All other title IDs are unknown.
204 # look up whether a title is currently available for checkout; returns a boolean value
205 sub do_availability_lookup {
208 # External ID for title. Depending on the API, this could be an ISBN
209 # or an identifier unique to that vendor.
210 my $title_id = shift;
212 # At this point, you would lookup title availability via an API request.
213 # In our case, since this is a test module, we just return availability info
214 # based on hard-coded values:
216 # 001 and 002 are unavailable.
217 return 0 if ($title_id eq '001');
218 return 0 if ($title_id eq '002');
221 return 1 if ($title_id eq '003');
223 # All other title IDs are unknown.
227 # check out a title to a patron
231 # External ID of title to be checked out.
232 my $title_id = shift;
234 # Patron ID or patron auth token, as returned by do_patron_auth().
235 my $user_token = shift;
237 # If checkout succeeds, the response is a hashref with the following fields:
239 # - xact_id (optional)
241 # If checkout fails, the response is a hashref with the following fields:
242 # - error_msg: a string containing an error message or description of why
243 # the checkout failed (e.g. "Checkout limit of (4) reached").
245 # If no valid response is received from the API, return undef.
247 # For testing purposes, user ID USER001 is our only valid user,
248 # and title 003 is the only available title.
249 if ($title_id && $user_token) {
250 if ($user_token eq 'USER001' && $title_id eq '003') {
251 return { due_date => DateTime->today()->add( days => 14 )->iso8601() };
253 return { msg => 'Checkout failed.' };
264 # External ID of title to be renewed.
265 my $title_id = shift;
267 # Patron ID or patron auth token, as returned by do_patron_auth().
268 my $user_token = shift;
270 # If renewal succeeds, the response is a hashref with the following fields:
272 # - xact_id (optional)
274 # If renewal fails, the response is a hashref with the following fields:
275 # - error_msg: a string containing an error message or description of why
276 # the renewal failed (e.g. "Renewal limit reached").
278 # If no valid response is received from the API, return undef.
280 # For testing purposes, user ID USER001 is our only valid user,
281 # and title 001 is the only renewable title.
282 if ($title_id && $user_token) {
283 if ($user_token eq 'USER001' && $title_id eq '001') {
284 return { due_date => DateTime->today()->add( days => 14 )->iso8601() };
286 return { error_msg => 'Renewal failed.' };
296 # External ID of title to be checked in.
297 my $title_id = shift;
299 # Patron ID or patron auth token, as returned by do_patron_auth().
300 my $user_token = shift;
302 # If checkin succeeds, return an empty hashref (actually it doesn't
303 # need to be empty, it just must NOT contain "error_msg" as a key).
305 # If checkin fails, return a hashref with the following fields:
306 # - error_msg: a string containing an error message or description of why
307 # the checkin failed (e.g. "Checkin failed").
309 # If no valid response is received from the API, return undef.
311 # For testing purposes, user ID USER001 is our only valid user,
312 # and title 003 is the only title that can be checked in.
313 if ($title_id && $user_token) {
314 if ($user_token eq 'USER001' && $title_id eq '003') {
317 return { error_msg => 'Checkin failed' };
327 # External ID of title to be held.
328 my $title_id = shift;
330 # Patron ID or patron auth token, as returned by do_patron_auth().
331 my $user_token = shift;
333 # If hold is successfully placed, return a hashref with the following
335 # - queue_position: this user's position in hold queue for this title
336 # - queue_size: total number of holds on this title
337 # - expire_date: when the hold expires
339 # If hold fails, return a hashref with the following fields:
340 # - error_msg: a string containing an error message or description of why
341 # the hold failed (e.g. "Hold limit (4) reached").
343 # If no valid response is received from the API, return undef.
345 # For testing purposes, we always and only allow placing a hold on title
346 # 002 by user ID USER001.
347 if ($title_id && $user_token) {
348 if ($user_token eq 'USER001' && $title_id eq '002') {
352 expire_date => DateTime->today()->add( days => 70 )->iso8601()
355 return { error_msg => 'Unable to place hold' };
365 # External ID of title.
366 my $title_id = shift;
368 # Patron ID or patron auth token, as returned by do_patron_auth().
369 my $user_token = shift;
371 # If hold is successfully canceled, return an empty hashref (actually it
372 # doesn't need to be empty, it just must NOT contain "error_msg" as a key).
374 # If hold is NOT canceled, return a hashref with the following fields:
375 # - error_msg: a string containing an error message or description of why
376 # the hold was not canceled (e.g. "Hold could not be canceled").
378 # If no valid response is received from the API, return undef.
380 # For testing purposes, we always and only allow canceling a hold on title
381 # 002 by user ID USER001.
382 if ($title_id && $user_token) {
383 if ($user_token eq 'USER001' && $title_id eq '002') {
386 return { error_msg => 'Unable to cancel hold' };
396 sub get_patron_checkouts {
399 # Patron ID or patron auth token.
400 my $user_token = shift;
402 # Return an array of hashrefs representing checkouts;
403 # each hashref should have the following keys:
404 # - xact_id: unique ID for this transaction (if used by API)
405 # - title_id: unique ID for this title
408 # - title: title of item, formatted for display
409 # - author: author of item, formatted for display
412 # USER001 is our only valid user, so we only return checkouts for them.
413 if ($user_token eq 'USER001') {
417 due_date => DateTime->today()->add( days => 7 )->iso8601(),
418 download_url => 'http://example.com/ebookapi/t/001/download',
419 title => 'The Fellowship of the Ring',
420 author => 'J. R. R. Tolkien'
423 $self->{checkouts} = $checkouts;
424 return $self->{checkouts};
427 sub get_patron_holds {
430 # Patron ID or patron auth token.
431 my $user_token = shift;
433 # Return an array of hashrefs representing holds;
434 # each hashref should have the following keys:
435 # - title_id: unique ID for this title
436 # - queue_position: this user's position in hold queue for this title
437 # - queue_size: total number of holds on this title
438 # - is_ready: whether hold is currently available for checkout
439 # - is_frozen: whether hold is suspended
440 # - thaw_date: when hold suspension expires (if suspended)
441 # - create_date: when the hold was placed
442 # - expire_date: when the hold expires
443 # - title: title of item, formatted for display
444 # - author: author of item, formatted for display
447 # USER001 is our only valid user, so we only return checkouts for them.
448 if ($user_token eq 'USER001') {
455 create_date => DateTime->today()->subtract( days => 10 )->iso8601(),
456 expire_date => DateTime->today()->add( days => 60 )->iso8601(),
457 title => 'The Two Towers',
458 author => 'J. R. R. Tolkien'
461 $self->{holds} = $holds;
462 return $self->{holds};