]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/Application/EbookAPI.pm
LP#1673870: Support placing and canceling OverDrive holds
[working/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, $email) = @_;
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     # place_hold has email as optional additional param
513     if ($action eq 'place_hold') {
514         $res = $handler->place_hold($title_id, $user_token, $email);
515     } else {
516         $res = $handler->$action($title_id, $user_token);
517     }
518     if (defined ($res)) {
519         return $res;
520     } else {
521         $logger->error("EbookAPI: could not do $action for title $title_id and patron $barcode");
522         return;
523     }
524 }
525 __PACKAGE__->register_method(
526     method => 'do_xact',
527     api_name => 'open-ils.ebook_api.checkout',
528     api_level => 1,
529     argc => 4,
530     signature => {
531         desc => "Checkout an ebook title to a patron",
532         params => [
533             {
534                 name => 'authtoken',
535                 desc => 'Authentication token',
536                 type => 'string'
537             },
538             {
539                 name => 'session_id',
540                 desc => 'The session ID (provided by open-ils.ebook_api.start_session)',
541                 type => 'string'
542             },
543             {
544                 name => 'title_id',
545                 desc => 'The identifier of the title',
546                 type => 'string'
547             },
548             {
549                 name => 'barcode',
550                 desc => 'The barcode of the patron to whom the title will be checked out',
551                 type => 'string'
552             },
553         ],
554         return => {
555             desc => 'Success: { due_date => "2017-01-01" } / Failure: { error_msg => "Checkout limit reached." }',
556             type => 'hashref'
557         }
558     }
559 );
560 __PACKAGE__->register_method(
561     method => 'do_xact',
562     api_name => 'open-ils.ebook_api.renew',
563     api_level => 1,
564     argc => 4,
565     signature => {
566         desc => "Renew an ebook title for a patron",
567         params => [
568             {
569                 name => 'authtoken',
570                 desc => 'Authentication token',
571                 type => 'string'
572             },
573             {
574                 name => 'session_id',
575                 desc => 'The session ID (provided by open-ils.ebook_api.start_session)',
576                 type => 'string'
577             },
578             {
579                 name => 'title_id',
580                 desc => 'The identifier of the title to be renewed',
581                 type => 'string'
582             },
583             {
584                 name => 'barcode',
585                 desc => 'The barcode of the patron to whom the title is checked out',
586                 type => 'string'
587             },
588         ],
589         return => {
590             desc => 'Success: { due_date => "2017-01-01" } / Failure: { error_msg => "Renewal limit reached." }',
591             type => 'hashref'
592         }
593     }
594 );
595 __PACKAGE__->register_method(
596     method => 'do_xact',
597     api_name => 'open-ils.ebook_api.checkin',
598     api_level => 1,
599     argc => 4,
600     signature => {
601         desc => "Check in an ebook title for a patron",
602         params => [
603             {
604                 name => 'authtoken',
605                 desc => 'Authentication token',
606                 type => 'string'
607             },
608             {
609                 name => 'session_id',
610                 desc => 'The session ID (provided by open-ils.ebook_api.start_session)',
611                 type => 'string'
612             },
613             {
614                 name => 'title_id',
615                 desc => 'The identifier of the title to be checked in',
616                 type => 'string'
617             },
618             {
619                 name => 'barcode',
620                 desc => 'The barcode of the patron to whom the title is checked out',
621                 type => 'string'
622             },
623         ],
624         return => {
625             desc => 'Success: { } / Failure: { error_msg => "Checkin failed." }',
626             type => 'hashref'
627         }
628     }
629 );
630 __PACKAGE__->register_method(
631     method => 'do_xact',
632     api_name => 'open-ils.ebook_api.place_hold',
633     api_level => 1,
634     argc => 4,
635     signature => {
636         desc => "Place a hold on an ebook title for a patron",
637         params => [
638             {
639                 name => 'authtoken',
640                 desc => 'Authentication token',
641                 type => 'string'
642             },
643             {
644                 name => 'session_id',
645                 desc => 'The session ID (provided by open-ils.ebook_api.start_session)',
646                 type => 'string'
647             },
648             {
649                 name => 'title_id',
650                 desc => 'The identifier of the title',
651                 type => 'string'
652             },
653             {
654                 name => 'barcode',
655                 desc => 'The barcode of the patron for whom the title is being held',
656                 type => 'string'
657             },
658         ],
659         return => {
660             desc => 'Success: { queue_position => 1, queue_size => 1, expire_date => "2017-01-01" } / Failure: { error_msg => "Could not place hold." }',
661             type => 'hashref'
662         }
663     }
664 );
665 __PACKAGE__->register_method(
666     method => 'do_xact',
667     api_name => 'open-ils.ebook_api.cancel_hold',
668     api_level => 1,
669     argc => 4,
670     signature => {
671         desc => "Cancel a hold on an ebook title for a patron",
672         params => [
673             {
674                 name => 'authtoken',
675                 desc => 'Authentication token',
676                 type => 'string'
677             },
678             {
679                 name => 'session_id',
680                 desc => 'The session ID (provided by open-ils.ebook_api.start_session)',
681                 type => 'string'
682             },
683             {
684                 name => 'title_id',
685                 desc => 'The identifier of the title',
686                 type => 'string'
687             },
688             {
689                 name => 'barcode',
690                 desc => 'The barcode of the patron',
691                 type => 'string'
692             },
693         ],
694         return => {
695             desc => 'Success: { } / Failure: { error_msg => "Could not cancel hold." }',
696             type => 'hashref'
697         }
698     }
699 );
700
701 sub _get_patron_xacts {
702     my ($xact_type, $auth, $session_id, $barcode) = @_;
703
704     $logger->info("EbookAPI: getting $xact_type for patron $barcode");
705
706     # verify that user is authenticated in EG
707     my $e = new_editor(authtoken => $auth);
708     if (!$e->checkauth) {
709         $logger->error("EbookAPI: authentication failed: " . $e->die_event);
710         return;
711     }
712
713     my $handler = new_handler($session_id);
714     my $user_token = $handler->do_patron_auth($barcode);
715
716     my $xacts;
717     if ($xact_type eq 'checkouts') {
718         $xacts = $handler->get_patron_checkouts($user_token);
719     } elsif ($xact_type eq 'holds') {
720         $xacts = $handler->get_patron_holds($user_token);
721     } else {
722         $logger->error("EbookAPI: invalid transaction type '$xact_type'");
723         return;
724     }
725
726     # cache and return transaction details
727     $handler->{$xact_type} = $xacts;
728     # Overlay transactions onto existing cached handler.
729     if (update_cache($handler, 1)) {
730         return $handler->{$xact_type};
731     } else {
732         $logger->error("EbookAPI: error caching transaction details ($xact_type)");
733         return;
734     }
735 }
736
737 sub get_patron_xacts {
738     my ($self, $conn, $auth, $session_id, $barcode) = @_;
739     my $xact_type;
740     if ($self->api_name =~ /checkouts/) {
741         $xact_type = 'checkouts';
742     } elsif ($self->api_name =~ /holds/) {
743         $xact_type = 'holds';
744     }
745     return _get_patron_xacts($xact_type, $auth, $session_id, $barcode);
746 }
747 __PACKAGE__->register_method(
748     method => 'get_patron_xacts',
749     api_name => 'open-ils.ebook_api.patron.get_checkouts',
750     api_level => 1,
751     argc => 3,
752     signature => {
753         desc => "Get information about a patron's ebook checkouts",
754         params => [
755             {
756                 name => 'authtoken',
757                 desc => 'Authentication token',
758                 type => 'string'
759             },
760             {
761                 name => 'session_id',
762                 desc => 'The session ID (provided by open-ils.ebook_api.start_session)',
763                 type => 'string'
764             },
765             {
766                 name => 'barcode',
767                 desc => 'The barcode of the patron',
768                 type => 'string'
769             }
770         ],
771         return => {
772             desc => 'Returns an array of transaction details, or undef if no details available',
773             type => 'array'
774         }
775     }
776 );
777 __PACKAGE__->register_method(
778     method => 'get_patron_xacts',
779     api_name => 'open-ils.ebook_api.patron.get_holds',
780     api_level => 1,
781     argc => 3,
782     signature => {
783         desc => "Get information about a patron's ebook holds",
784         params => [
785             {
786                 name => 'authtoken',
787                 desc => 'Authentication token',
788                 type => 'string'
789             },
790             {
791                 name => 'session_id',
792                 desc => 'The session ID (provided by open-ils.ebook_api.start_session)',
793                 type => 'string'
794             },
795             {
796                 name => 'barcode',
797                 desc => 'The barcode of the patron',
798                 type => 'string'
799             }
800         ],
801         return => {
802             desc => 'Returns an array of transaction details, or undef if no details available',
803             type => 'array'
804         }
805     }
806 );
807
808 sub get_all_patron_xacts {
809     my ($self, $conn, $auth, $session_id, $barcode) = @_;
810     my $checkouts = _get_patron_xacts('checkouts', $auth, $session_id, $barcode);
811     my $holds = _get_patron_xacts('holds', $auth, $session_id, $barcode);
812     return {
813         checkouts => $checkouts,
814         holds     => $holds
815     };
816 }
817 __PACKAGE__->register_method(
818     method => 'get_all_patron_xacts',
819     api_name => 'open-ils.ebook_api.patron.get_transactions',
820     api_level => 1,
821     argc => 3,
822     signature => {
823         desc => "Get information about a patron's ebook checkouts and holds",
824         params => [
825             {
826                 name => 'authtoken',
827                 desc => 'Authentication token',
828                 type => 'string'
829             },
830             {
831                 name => 'session_id',
832                 desc => 'The session ID (provided by open-ils.ebook_api.start_session)',
833                 type => 'string'
834             },
835             {
836                 name => 'barcode',
837                 desc => 'The barcode of the patron',
838                 type => 'string'
839             }
840         ],
841         return => {
842             desc => 'Returns a hashref of transactions: { checkouts => [], holds => [], failed => [] }',
843             type => 'hashref'
844         }
845     }
846 );
847
848 1;