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