]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/Application/EbookAPI.pm
LP#1776954 Avoid empty string for tcn_source
[Evergreen.git] / Open-ILS / src / perlmods / lib / OpenILS / Application / EbookAPI.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 # We define a handler class for each vendor API (OneClickdigital, OverDrive, etc.).
21 # See EbookAPI/Test.pm for a reference implementation with required methods,
22 # arguments, and return values.
23 # ====================================================================== 
24
25 package OpenILS::Application::EbookAPI;
26
27 use strict;
28 use warnings;
29
30 use Time::HiRes qw/gettimeofday/;
31 use Digest::MD5 qw/md5_hex/;
32
33 use OpenILS::Application;
34 use base qw/OpenILS::Application/;
35 use OpenSRF::AppSession;
36 use OpenILS::Utils::CStoreEditor qw/:funcs/;
37 use OpenSRF::EX qw(:try);
38 use OpenSRF::Utils::SettingsClient;
39 use OpenSRF::Utils::Logger qw($logger);
40 use OpenSRF::Utils::Cache;
41 use OpenSRF::Utils::JSON;
42 use OpenILS::Utils::HTTPClient;
43
44 my $handler;
45 my $cache;
46 my $cache_timeout;
47 my $default_request_timeout;
48
49 # map EbookAPI vendor codes to corresponding packages
50 our %vendor_handlers = (
51     'ebook_test' => 'OpenILS::Application::EbookAPI::Test',
52     'oneclickdigital' => 'OpenILS::Application::EbookAPI::OneClickdigital',
53     'overdrive' => 'OpenILS::Application::EbookAPI::OverDrive'
54 );
55
56 sub initialize {
57     $cache = OpenSRF::Utils::Cache->new;
58
59     my $sclient = OpenSRF::Utils::SettingsClient->new();
60     $cache_timeout = $sclient->config_value("apps", "open-ils.ebook_api", "app_settings", "cache_timeout" ) || 300;
61     $default_request_timeout = $sclient->config_value("apps", "open-ils.ebook_api", "app_settings", "request_timeout" ) || 60;
62 }
63
64 # returns the cached object (if successful)
65 sub update_cache {
66     my $cache_obj = shift;
67     my $overlay = shift || 0;
68     my $cache_key;
69     if ($cache_obj->{session_id}) {
70         $cache_key = $cache_obj->{session_id};
71     } else {
72         $logger->error("EbookAPI: cannot update cache with unknown cache object");
73         return;
74     }
75
76     # Optionally, keep old cached field values unless a new value for that
77     # field is explicitly provided.  This makes it easier for asynchronous
78     # requests (e.g. for circs and holds) to cache their results.
79     if ($overlay) {
80         if (my $orig_cache = $cache->get_cache($cache_key)) {
81             $logger->info("EbookAPI: overlaying new values on existing cache object");
82             foreach my $k (%$cache_obj) {
83                 # Add/overwrite existing cached value if a new value is defined.
84                 $orig_cache->{$k} = $cache_obj->{$k} if (defined $cache_obj->{$k});
85             }
86             # The cache object we want to save is the (updated) original one.
87             $cache_obj = $orig_cache;
88         }
89     }
90
91     try { # fail silently if there's no pre-existing cache to delete
92         $cache->delete_cache($cache_key);
93     } catch Error with {};
94     if (my $success_key = $cache->put_cache($cache_key, $cache_obj, $cache_timeout)) {
95         return $cache->get_cache($success_key);
96     } else {
97         $logger->error("EbookAPI: error when updating cache with object");
98         return;
99     }
100 }
101
102 sub retrieve_session {
103     my $session_id = shift;
104     unless ($session_id) {
105         $logger->info("EbookAPI: no session ID provided");
106         return;
107     }
108     my $cached_session = $cache->get_cache($session_id) || undef;
109     if ($cached_session) {
110         return $cached_session;
111     } else {
112         $logger->info("EbookAPI: could not find cached session with id $session_id");
113         return;
114     }
115 }
116
117 # prepare new handler from session
118 # (will retrieve cached session unless a session object is provided)
119 sub new_handler {
120     my $session_id = shift;
121     my $ses = shift || retrieve_session($session_id);
122     if (!$ses) {
123         $logger->error("EbookAPI: could not start handler - no cached session with ID $session_id");
124         return;
125     }
126     my $module = ref($ses);
127     $logger->info("EbookAPI: starting new $module handler from cached session $session_id...");
128     $module->use;
129     my $handler = $module->new($ses);
130     return $handler;
131 }
132
133
134 sub check_session {
135     my $self = shift;
136     my $conn = shift;
137     my $session_id = shift;
138     my $vendor = shift;
139     my $ou = shift;
140
141     return start_session($self, $conn, $vendor, $ou) unless $session_id;
142
143     my $cached_session = retrieve_session($session_id);
144     if ($cached_session) {
145         # re-authorize cached session, if applicable
146         my $handler = new_handler($session_id, $cached_session);
147         $handler->do_client_auth();
148         if (update_cache($handler)) {
149             return $session_id;
150         } else {
151             $logger->error("EbookAPI: error updating session cache");
152             return;
153         }
154     } else {
155         return start_session($self, $conn, $vendor, $ou);
156     }
157 }
158 __PACKAGE__->register_method(
159     method => 'check_session',
160     api_name => 'open-ils.ebook_api.check_session',
161     api_level => 1,
162     argc => 2,
163     signature => {
164         desc => "Validate an existing EbookAPI session, or initiate a new one",
165         params => [
166             {
167                 name => 'session_id',
168                 desc => 'The EbookAPI session ID being checked',
169                 type => 'string'
170             },
171             {
172                 name => 'vendor',
173                 desc => 'The ebook vendor (e.g. "oneclickdigital")',
174                 type => 'string'
175             },
176             {
177                 name => 'ou',
178                 desc => 'The context org unit ID',
179                 type => 'number'
180             }
181         ],
182         return => {
183             desc => 'Returns an EbookAPI session ID',
184             type => 'string'
185         }
186     }
187 );
188
189 sub _start_session {
190     my $vendor = shift;
191     my $ou = shift;
192     $ou = $ou || 1; # default to top-level org unit
193
194     my $module;
195     
196     # determine EbookAPI handler from vendor name
197     # TODO handle API versions?
198     if ($vendor_handlers{$vendor}) {
199         $module = $vendor_handlers{$vendor};
200     } else {
201         $logger->error("EbookAPI: No handler module found for $vendor!");
202         return;
203     }
204
205     # TODO cache session? reuse an existing one if available?
206
207     # generate session ID
208     my ($sec, $usec) = gettimeofday();
209     my $r = rand();
210     my $session_id = "ebook_api.ses." . md5_hex("$sec-$usec-$r");
211     
212     my $args = {
213         vendor => $vendor,
214         ou => $ou,
215         session_id => $session_id
216     };
217
218     $module->use;
219     $handler = $module->new($args);  # create new handler object
220     $handler->initialize();          # set handler attributes
221     $handler->do_client_auth();      # authorize client session against API, if applicable
222
223     # our "session" is actually just our handler object, serialized and cached
224     my $ckey = $handler->{session_id};
225     $cache->put_cache($ckey, $handler, $cache_timeout);
226
227     return $handler->{session_id};
228 }
229
230 sub start_session {
231     my $self = shift;
232     my $conn = shift;
233     my $vendor = shift;
234     my $ou = shift;
235     return _start_session($vendor, $ou);
236 }
237 __PACKAGE__->register_method(
238     method => 'start_session',
239     api_name => 'open-ils.ebook_api.start_session',
240     api_level => 1,
241     argc => 1,
242     signature => {
243         desc => "Initiate an EbookAPI session",
244         params => [
245             {
246                 name => 'vendor',
247                 desc => 'The ebook vendor (e.g. "oneclickdigital")',
248                 type => 'string'
249             },
250             {
251                 name => 'ou',
252                 desc => 'The context org unit ID',
253                 type => 'number'
254             }
255         ],
256         return => {
257             desc => 'Returns an EbookAPI session ID',
258             type => 'string'
259         }
260     }
261 );
262
263 sub cache_patron_password {
264     my $self = shift;
265     my $conn = shift;
266     my $session_id = shift;
267     my $password = shift;
268
269     # We don't need the handler module for this.
270     # Let's just update the cache directly.
271     if (my $ses = $cache->get_cache($session_id)) {
272         $ses->{patron_password} = $password;
273         if (update_cache($ses)) {
274             return $session_id;
275         } else {
276             $logger->error("EbookAPI: there was an error caching patron password");
277             return;
278         }
279     }
280 }
281 __PACKAGE__->register_method(
282     method => 'cache_patron_password',
283     api_name => 'open-ils.ebook_api.patron.cache_password',
284     api_level => 1,
285     argc => 2,
286     signature => {
287         desc => "Cache patron password on login for use during EbookAPI patron authentication",
288         params => [
289             {
290                 name => 'session_id',
291                 desc => 'The session ID (provided by open-ils.ebook_api.start_session)',
292                 type => 'string'
293             },
294             {
295                 name => 'patron_password',
296                 desc => 'The patron password',
297                 type => 'string'
298             }
299         ],
300         return => { desc => 'A session key, or undef' }
301     }
302 );
303
304 # Submit an HTTP request to a specified API endpoint.
305 #
306 # Params:
307 #
308 #   $req - hashref containing the following:
309 #       method: HTTP request method (defaults to GET)
310 #       uri: API endpoint URI (required)
311 #       header: arrayref of HTTP headers (optional, but see below)
312 #       content: content of HTTP request (optional)
313 #       request_timeout (defaults to value in opensrf.xml)
314 #   $session_id - id of cached EbookAPI session
315 #
316 # A "Content-Type: application/json" header is automatically added to each
317 # request.  If no Authorization header is provided via the $req param, the
318 # following header will also be automatically added:
319 #
320 #   Authorization: basic $basic_token
321 #
322 # ... where $basic_token is derived from the cached session identified by the
323 # $session_id param.  If this does not meet the needs of your API, include the
324 # correct Authorization header in $req->{header}.
325 sub request {
326     my $self = shift;
327     my $req = shift;
328     my $session_id = shift;
329
330     my $uri;
331     if (!defined ($req->{uri})) {
332         $logger->error('EbookAPI: attempted an HTTP request but no URI was provided');
333         return;
334     } else {
335         $uri = $req->{uri};
336     }
337     
338     my $method = defined $req->{method} ? $req->{method} : 'GET';
339     my $headers = defined $req->{headers} ? $req->{headers} : {};
340     my $content = defined $req->{content} ? $req->{content} : undef;
341     my $request_timeout = defined $req->{request_timeout} ? $req->{request_timeout} : $default_request_timeout;
342
343     # JSON as default content type
344     if ( !defined ($headers->{'Content-Type'}) ) {
345         $headers->{'Content-Type'} = 'application/json';
346     }
347
348     # all requests also require an Authorization header;
349     # let's default to using our basic token, if available
350     if ( !defined ($headers->{'Authorization'}) ) {
351         if (!$session_id) {
352             $logger->error("EbookAPI: HTTP request requires session info but no session ID was provided");
353             return;
354         }
355         my $ses = retrieve_session($session_id);
356         if ($ses) {
357             my $basic_token = $ses->{basic_token};
358             $headers->{'Authorization'} = "basic $basic_token";
359         }
360     }
361
362     my $client = OpenILS::Utils::HTTPClient->new();
363     my $res = $client->request(
364         $method,
365         $uri,
366         $headers,
367         $content,
368         $request_timeout
369     );
370     if (!defined ($res)) {
371         $logger->error('EbookAPI: no HTTP response received');
372         return;
373     } else {
374         $logger->info("EbookAPI: response received from server: " . $res->status_line);
375         return {
376             is_success => $res->is_success,
377             status     => $res->status_line,
378             content    => OpenSRF::Utils::JSON->JSON2perl($res->decoded_content)
379         };
380     }
381 }
382
383 sub get_details {
384     my ($self, $conn, $session_id, $title_id) = @_;
385     my $handler = new_handler($session_id);
386     return $handler->get_title_info($title_id);
387 }
388 __PACKAGE__->register_method(
389     method => 'get_details',
390     api_name => 'open-ils.ebook_api.title.details',
391     api_level => 1,
392     argc => 2,
393     signature => {
394         desc => "Get basic metadata for an ebook title",
395         params => [
396             {
397                 name => 'session_id',
398                 desc => 'The session ID (provided by open-ils.ebook_api.start_session)',
399                 type => 'string'
400             },
401             {
402                 name => 'title_id',
403                 desc => 'The title ID (ISBN, unique identifier, etc.)',
404                 type => 'string'
405             }
406         ],
407         return => {
408             desc => 'Success: { title => "Title", author => "Author Name" } / Failure: { error => "Title not found" }',
409             type => 'hashref'
410         }
411     }
412 );
413
414 sub get_availability {
415     my ($self, $conn, $session_id, $title_id) = @_;
416     my $handler = new_handler($session_id);
417     return $handler->do_availability_lookup($title_id);
418 }
419 __PACKAGE__->register_method(
420     method => 'get_availability',
421     api_name => 'open-ils.ebook_api.title.availability',
422     api_level => 1,
423     argc => 2,
424     signature => {
425         desc => "Get availability info for an ebook title",
426         params => [
427             {
428                 name => 'session_id',
429                 desc => 'The session ID (provided by open-ils.ebook_api.start_session)',
430                 type => 'string'
431             },
432             {
433                 name => 'title_id',
434                 desc => 'The title ID (ISBN, unique identifier, etc.)',
435                 type => 'string'
436             }
437         ],
438         return => {
439             desc => 'Returns 1 if title is available, 0 if not available, or undef if availability info could not be retrieved',
440             type => 'number'
441         }
442     }
443 );
444
445 sub get_holdings {
446     my ($self, $conn, $session_id, $title_id) = @_;
447     my $handler = new_handler($session_id);
448     return $handler->do_holdings_lookup($title_id);
449 }
450 __PACKAGE__->register_method(
451     method => 'get_holdings',
452     api_name => 'open-ils.ebook_api.title.holdings',
453     api_level => 1,
454     argc => 2,
455     signature => {
456         desc => "Get detailed holdings info (copy counts and formats) for an ebook title, or basic availability if holdings info is unavailable",
457         params => [
458             {
459                 name => 'session_id',
460                 desc => 'The session ID (provided by open-ils.ebook_api.start_session)',
461                 type => 'string'
462             },
463             {
464                 name => 'title_id',
465                 desc => 'The title ID (ISBN, unique identifier, etc.)',
466                 type => 'string'
467             }
468         ],
469         return => {
470             desc => 'Returns a hashref of holdings info with one or more of the following keys: available (0 or 1), copies_owned, copies_available, formats (arrayref of strings)',
471             type => 'hashref'
472         }
473     }
474 );
475
476 # Wrapper function for performing transactions that require an authenticated
477 # patron and a title identifier (checkout, checkin, renewal, etc).
478 #
479 # Params:
480 # - title_id: ISBN (OneClickdigital), title identifier (OverDrive)
481 # - barcode: patron barcode
482 #
483 sub do_xact {
484     my ($self, $conn, $auth, $session_id, $title_id, $barcode, $param) = @_;
485
486     my $action;
487     if ($self->api_name =~ /checkout/) {
488         $action = 'checkout';
489     } elsif ($self->api_name =~ /checkin/) {
490         $action = 'checkin';
491     } elsif ($self->api_name =~ /renew/) {
492         $action = 'renew';
493     } elsif ($self->api_name =~ /place_hold/) {
494         $action = 'place_hold';
495     } elsif ($self->api_name =~ /cancel_hold/) {
496         $action = 'cancel_hold';
497     }
498     $logger->info("EbookAPI: doing $action for title $title_id...");
499
500     # verify that user is authenticated in EG
501     my $e = new_editor(authtoken => $auth);
502     if (!$e->checkauth) {
503         $logger->error("EbookAPI: authentication failed: " . $e->die_event);
504         return;
505     }
506
507     my $handler = new_handler($session_id);
508     my $user_token = $handler->do_patron_auth($barcode);
509
510     # handler method constructs and submits request (and handles any external authentication)
511     my $res;
512     if ($action eq 'checkout') {
513         # checkout has format as optional additional param
514         $res = $handler->checkout($title_id, $user_token, $param);
515     } elsif ($action eq 'place_hold') {
516         # place_hold has email as optional additional param
517         $res = $handler->place_hold($title_id, $user_token, $param);
518     } else {
519         $res = $handler->$action($title_id, $user_token);
520     }
521     if (defined ($res)) {
522         return $res;
523     } else {
524         $logger->error("EbookAPI: could not do $action for title $title_id and patron $barcode");
525         return;
526     }
527 }
528 __PACKAGE__->register_method(
529     method => 'do_xact',
530     api_name => 'open-ils.ebook_api.checkout',
531     api_level => 1,
532     argc => 4,
533     signature => {
534         desc => "Checkout an ebook title to a patron",
535         params => [
536             {
537                 name => 'authtoken',
538                 desc => 'Authentication token',
539                 type => 'string'
540             },
541             {
542                 name => 'session_id',
543                 desc => 'The session ID (provided by open-ils.ebook_api.start_session)',
544                 type => 'string'
545             },
546             {
547                 name => 'title_id',
548                 desc => 'The identifier of the title',
549                 type => 'string'
550             },
551             {
552                 name => 'barcode',
553                 desc => 'The barcode of the patron to whom the title will be checked out',
554                 type => 'string'
555             },
556         ],
557         return => {
558             desc => 'Success: { due_date => "2017-01-01" } / Failure: { error_msg => "Checkout limit reached." }',
559             type => 'hashref'
560         }
561     }
562 );
563 __PACKAGE__->register_method(
564     method => 'do_xact',
565     api_name => 'open-ils.ebook_api.renew',
566     api_level => 1,
567     argc => 4,
568     signature => {
569         desc => "Renew an ebook title for a patron",
570         params => [
571             {
572                 name => 'authtoken',
573                 desc => 'Authentication token',
574                 type => 'string'
575             },
576             {
577                 name => 'session_id',
578                 desc => 'The session ID (provided by open-ils.ebook_api.start_session)',
579                 type => 'string'
580             },
581             {
582                 name => 'title_id',
583                 desc => 'The identifier of the title to be renewed',
584                 type => 'string'
585             },
586             {
587                 name => 'barcode',
588                 desc => 'The barcode of the patron to whom the title is checked out',
589                 type => 'string'
590             },
591         ],
592         return => {
593             desc => 'Success: { due_date => "2017-01-01" } / Failure: { error_msg => "Renewal limit reached." }',
594             type => 'hashref'
595         }
596     }
597 );
598 __PACKAGE__->register_method(
599     method => 'do_xact',
600     api_name => 'open-ils.ebook_api.checkin',
601     api_level => 1,
602     argc => 4,
603     signature => {
604         desc => "Check in an ebook title for a patron",
605         params => [
606             {
607                 name => 'authtoken',
608                 desc => 'Authentication token',
609                 type => 'string'
610             },
611             {
612                 name => 'session_id',
613                 desc => 'The session ID (provided by open-ils.ebook_api.start_session)',
614                 type => 'string'
615             },
616             {
617                 name => 'title_id',
618                 desc => 'The identifier of the title to be checked in',
619                 type => 'string'
620             },
621             {
622                 name => 'barcode',
623                 desc => 'The barcode of the patron to whom the title is checked out',
624                 type => 'string'
625             },
626         ],
627         return => {
628             desc => 'Success: { } / Failure: { error_msg => "Checkin failed." }',
629             type => 'hashref'
630         }
631     }
632 );
633 __PACKAGE__->register_method(
634     method => 'do_xact',
635     api_name => 'open-ils.ebook_api.place_hold',
636     api_level => 1,
637     argc => 4,
638     signature => {
639         desc => "Place a hold on an ebook title for a patron",
640         params => [
641             {
642                 name => 'authtoken',
643                 desc => 'Authentication token',
644                 type => 'string'
645             },
646             {
647                 name => 'session_id',
648                 desc => 'The session ID (provided by open-ils.ebook_api.start_session)',
649                 type => 'string'
650             },
651             {
652                 name => 'title_id',
653                 desc => 'The identifier of the title',
654                 type => 'string'
655             },
656             {
657                 name => 'barcode',
658                 desc => 'The barcode of the patron for whom the title is being held',
659                 type => 'string'
660             },
661         ],
662         return => {
663             desc => 'Success: { queue_position => 1, queue_size => 1, expire_date => "2017-01-01" } / Failure: { error_msg => "Could not place hold." }',
664             type => 'hashref'
665         }
666     }
667 );
668 __PACKAGE__->register_method(
669     method => 'do_xact',
670     api_name => 'open-ils.ebook_api.cancel_hold',
671     api_level => 1,
672     argc => 4,
673     signature => {
674         desc => "Cancel a hold on an ebook title for a patron",
675         params => [
676             {
677                 name => 'authtoken',
678                 desc => 'Authentication token',
679                 type => 'string'
680             },
681             {
682                 name => 'session_id',
683                 desc => 'The session ID (provided by open-ils.ebook_api.start_session)',
684                 type => 'string'
685             },
686             {
687                 name => 'title_id',
688                 desc => 'The identifier of the title',
689                 type => 'string'
690             },
691             {
692                 name => 'barcode',
693                 desc => 'The barcode of the patron',
694                 type => 'string'
695             },
696         ],
697         return => {
698             desc => 'Success: { } / Failure: { error_msg => "Could not cancel hold." }',
699             type => 'hashref'
700         }
701     }
702 );
703
704 sub _get_patron_xacts {
705     my ($xact_type, $auth, $session_id, $barcode) = @_;
706
707     $logger->info("EbookAPI: getting $xact_type for patron $barcode");
708
709     # verify that user is authenticated in EG
710     my $e = new_editor(authtoken => $auth);
711     if (!$e->checkauth) {
712         $logger->error("EbookAPI: authentication failed: " . $e->die_event);
713         return;
714     }
715
716     my $handler = new_handler($session_id);
717     my $user_token = $handler->do_patron_auth($barcode);
718
719     my $xacts;
720     if ($xact_type eq 'checkouts') {
721         $xacts = $handler->get_patron_checkouts($user_token);
722     } elsif ($xact_type eq 'holds') {
723         $xacts = $handler->get_patron_holds($user_token);
724     } else {
725         $logger->error("EbookAPI: invalid transaction type '$xact_type'");
726         return;
727     }
728
729     # cache and return transaction details
730     $handler->{$xact_type} = $xacts;
731     # Overlay transactions onto existing cached handler.
732     if (update_cache($handler, 1)) {
733         return $handler->{$xact_type};
734     } else {
735         $logger->error("EbookAPI: error caching transaction details ($xact_type)");
736         return;
737     }
738 }
739
740 sub get_patron_xacts {
741     my ($self, $conn, $auth, $session_id, $barcode) = @_;
742     my $xact_type;
743     if ($self->api_name =~ /checkouts/) {
744         $xact_type = 'checkouts';
745     } elsif ($self->api_name =~ /holds/) {
746         $xact_type = 'holds';
747     }
748     return _get_patron_xacts($xact_type, $auth, $session_id, $barcode);
749 }
750 __PACKAGE__->register_method(
751     method => 'get_patron_xacts',
752     api_name => 'open-ils.ebook_api.patron.get_checkouts',
753     api_level => 1,
754     argc => 3,
755     signature => {
756         desc => "Get information about a patron's ebook checkouts",
757         params => [
758             {
759                 name => 'authtoken',
760                 desc => 'Authentication token',
761                 type => 'string'
762             },
763             {
764                 name => 'session_id',
765                 desc => 'The session ID (provided by open-ils.ebook_api.start_session)',
766                 type => 'string'
767             },
768             {
769                 name => 'barcode',
770                 desc => 'The barcode of the patron',
771                 type => 'string'
772             }
773         ],
774         return => {
775             desc => 'Returns an array of transaction details, or undef if no details available',
776             type => 'array'
777         }
778     }
779 );
780 __PACKAGE__->register_method(
781     method => 'get_patron_xacts',
782     api_name => 'open-ils.ebook_api.patron.get_holds',
783     api_level => 1,
784     argc => 3,
785     signature => {
786         desc => "Get information about a patron's ebook holds",
787         params => [
788             {
789                 name => 'authtoken',
790                 desc => 'Authentication token',
791                 type => 'string'
792             },
793             {
794                 name => 'session_id',
795                 desc => 'The session ID (provided by open-ils.ebook_api.start_session)',
796                 type => 'string'
797             },
798             {
799                 name => 'barcode',
800                 desc => 'The barcode of the patron',
801                 type => 'string'
802             }
803         ],
804         return => {
805             desc => 'Returns an array of transaction details, or undef if no details available',
806             type => 'array'
807         }
808     }
809 );
810
811 sub get_all_patron_xacts {
812     my ($self, $conn, $auth, $session_id, $barcode) = @_;
813     my $checkouts = _get_patron_xacts('checkouts', $auth, $session_id, $barcode);
814     my $holds = _get_patron_xacts('holds', $auth, $session_id, $barcode);
815     return {
816         checkouts => $checkouts,
817         holds     => $holds
818     };
819 }
820 __PACKAGE__->register_method(
821     method => 'get_all_patron_xacts',
822     api_name => 'open-ils.ebook_api.patron.get_transactions',
823     api_level => 1,
824     argc => 3,
825     signature => {
826         desc => "Get information about a patron's ebook checkouts and holds",
827         params => [
828             {
829                 name => 'authtoken',
830                 desc => 'Authentication token',
831                 type => 'string'
832             },
833             {
834                 name => 'session_id',
835                 desc => 'The session ID (provided by open-ils.ebook_api.start_session)',
836                 type => 'string'
837             },
838             {
839                 name => 'barcode',
840                 desc => 'The barcode of the patron',
841                 type => 'string'
842             }
843         ],
844         return => {
845             desc => 'Returns a hashref of transactions: { checkouts => [], holds => [], failed => [] }',
846             type => 'hashref'
847         }
848     }
849 );
850
851 sub get_download_link {
852     my ($self, $conn, $auth, $session_id, $request_link) = @_;
853     my $handler = new_handler($session_id);
854     return $handler->do_get_download_link($request_link);
855 }
856 __PACKAGE__->register_method(
857     method => 'get_download_link',
858     api_name => 'open-ils.ebook_api.title.get_download_link',
859     api_level => 1,
860     argc => 3,
861     signature => {
862         desc => "Get download link for an OverDrive title that has been checked out",
863         params => [
864             {
865                 name => 'authtoken',
866                 desc => 'Authentication token',
867                 type => 'string'
868             },
869             {
870                 name => 'session_id',
871                 desc => 'The session ID (provided by open-ils.ebook_api.start_session)',
872                 type => 'string'
873             },
874             {
875                 name => 'request_link',
876                 desc => 'The URL used to request a download link',
877                 type => 'string'
878             }
879         ],
880         return => {
881             desc => 'Success: { url => "http://example.com/download-link" } / Failure: { error_msg => "Download link request failed." }',
882             type => 'hashref'
883         }
884     }
885 );
886
887 1;