LP#1673870: fix OneClickdigital handler to reflect how their API actually works,...
[working/Evergreen.git] / Open-ILS / src / perlmods / lib / OpenILS / Application / EbookAPI / OneClickdigital.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 package OpenILS::Application::EbookAPI::OneClickdigital;
20
21 use strict;
22 use warnings;
23
24 use OpenILS::Application;
25 use OpenILS::Application::EbookAPI;
26 use base qw/OpenILS::Application::EbookAPI/;
27 use OpenSRF::AppSession;
28 use OpenSRF::EX qw(:try);
29 use OpenSRF::Utils::SettingsClient;
30 use OpenSRF::Utils::Logger qw($logger);
31 use OpenSRF::Utils::Cache;
32 use OpenILS::Application::AppUtils;
33 use Data::Dumper;
34
35 sub new {
36     my( $class, $args ) = @_;
37     $class = ref $class || $class;
38     return bless $args, $class;
39 }
40
41 sub ou {
42     my $self = shift;
43     return $self->{ou};
44 }
45
46 sub vendor {
47     my $self = shift;
48     return $self->{vendor};
49 }
50
51 sub session_id {
52     my $self = shift;
53     return $self->{session_id};
54 }
55
56 sub base_uri {
57     my $self = shift;
58     return $self->{base_uri};
59 }
60
61 sub library_id {
62     my $self = shift;
63     return $self->{library_id};
64 }
65
66 sub basic_token {
67     my $self = shift;
68     return $self->{basic_token};
69 }
70
71 sub patron_id {
72     my $self = shift;
73     return $self->{patron_id};
74 }
75
76 sub initialize {
77     my $self = shift;
78     my $ou = $self->{ou};
79
80     my $base_uri = 'https://api.oneclickdigital.com/v1';
81     $self->{base_uri} = OpenILS::Application::AppUtils->ou_ancestor_setting_value($ou, 'ebook_api.oneclickdigital.base_uri') || $base_uri;
82
83     my $library_id = OpenILS::Application::AppUtils->ou_ancestor_setting_value($ou, 'ebook_api.oneclickdigital.library_id');
84     if ($library_id) {
85         $self->{library_id} = $library_id;
86     } else {
87         $logger->error("EbookAPI: no OneClickdigital library ID found for org unit $ou");
88         return;
89     }
90
91     my $basic_token = OpenILS::Application::AppUtils->ou_ancestor_setting_value($ou, 'ebook_api.oneclickdigital.basic_token');
92     if ($basic_token) {
93         $self->{basic_token} = $basic_token;
94     } else {
95         $logger->error("EbookAPI: no OneClickdigital basic token found for org unit $ou");
96         return;
97     }
98
99     return $self;
100
101 }
102
103 # OneClickdigital API does not require separate client auth;
104 # we just need to include our basic auth token in requests
105 sub do_client_auth {
106     my $self = shift;
107     return;
108 }
109
110 # retrieve OneClickdigital patron ID (if any) based on patron barcode
111 # GET http://api.oneclickdigital.us/v1/rpc/libraries/{libraryID}/patrons/{barcode}
112 sub do_patron_auth {
113     my ($self, $barcode) = @_;
114     my $base_uri = $self->{base_uri};
115     my $library_id = $self->{library_id};
116     my $session_id = $self->{session_id};
117     my $req = {
118         method => 'GET',
119         uri    => "$base_uri/rpc/libraries/$library_id/patrons/$barcode"
120     };
121     my $res = $self->request($req, $session_id);
122     # TODO distinguish between unregistered patrons and patron auth failure
123     if (defined ($res) && $res->{content}->{patronId}) {
124         return $res->{content}->{patronId};
125     }
126     $logger->error("EbookAPI: no OneClickdigital patron ID found for barcode $barcode");
127     return;
128 }
129
130 # get basic metadata for an item (title, author, cover image if any)
131 # GET http://api.oneclickdigital.us/v1/libraries/{libraryId}/media/{isbn}
132 sub get_title_info {
133     my ($self, $isbn) = @_;
134     my $base_uri = $self->{base_uri};
135     my $library_id = $self->{library_id};
136     my $session_id = $self->{session_id};
137     my $req = {
138         method => 'GET',
139         uri    => "$base_uri/libraries/$library_id/media/$isbn"
140     };
141     my $res = $self->request($req, $session_id);
142     if (defined ($res)) {
143         return {
144             title  => $res->{content}->{title},
145             author => $res->{content}->{authors}
146         };
147     } else {
148         $logger->error("EbookAPI: could not retrieve OneClickdigital title details for ISBN $isbn");
149         return;
150     }
151 }
152
153 # does this title have available "copies"? y/n
154 # GET http://api.oneclickdigital.us/v1/libraries/{libraryID}/media/{isbn}/availability
155 sub do_availability_lookup {
156     my ($self, $isbn) = @_;
157     my $base_uri = $self->{base_uri};
158     my $library_id = $self->{library_id};
159     my $session_id = $self->{session_id};
160     my $req = {
161         method => 'GET',
162         uri    => "$base_uri/libraries/$library_id/media/$isbn/availability"
163     };
164     my $res = $self->request($req, $session_id);
165     if (defined ($res)) {
166         $logger->info("EbookAPI: received availability response for ISBN $isbn: " . Dumper $res);
167         return $res->{content}->{availability};
168     } else {
169         $logger->error("EbookAPI: could not retrieve OneClickdigital availability for ISBN $isbn");
170         return;
171     }
172 }
173
174 # OneClickdigital API does not support detailed holdings lookup,
175 # so we return basic availability information.
176 sub do_holdings_lookup {
177     my ($self, $isbn) = @_;
178     my $avail = $self->do_availability_lookup($isbn);
179     return { available => $avail };
180 }
181
182 # checkout an item to a patron
183 # item is identified by ISBN, patron ID is their barcode
184 # POST //api.{domain}/v1/libraries/{libraryId}/patrons/{patronId}/checkouts/{isbn}
185 sub checkout {
186     my ($self, $isbn, $patron_id) = @_;
187     my $base_uri = $self->{base_uri};
188     my $library_id = $self->{library_id};
189     my $session_id = $self->{session_id};
190     my $req = {
191         method => 'POST',
192         uri    => "$base_uri/libraries/$library_id/patrons/$patron_id/checkouts/$isbn",
193         headers => { "Content-Length" => "0" }
194     };
195     my $res = $self->request($req, $session_id);
196
197     # TODO: more sophisticated response handling
198     # HTTP 200 response indicates success, HTTP 409 indicates checkout limit reached
199     if (defined ($res)) {
200         if ($res->{is_success}) {
201             return {
202                 xact_id => $res->{content}->{transactionId},
203                 due_date => $res->{content}->{expiration}
204             };
205         } else {
206             $logger->error("EbookAPI: checkout failed for OneClickdigital title $isbn");
207             return { error_msg => $res->{content} };
208         }
209     } else {
210         $logger->error("EbookAPI: no response received from OneClickdigital server");
211         return;
212     }
213 }
214
215 # renew a checked-out item
216 # item id = ISBN, patron id = barcode
217 # PUT //api.{domain}/v1/libraries/{libraryId}/patrons/{patronId}/checkouts/{isbn}
218 sub renew {
219     my ($self, $isbn, $patron_id) = @_;
220     my $base_uri = $self->{base_uri};
221     my $library_id = $self->{library_id};
222     my $session_id = $self->{session_id};
223     my $req = {
224         method => 'PUT',
225         uri    => "$base_uri/libraries/$library_id/patrons/$patron_id/checkouts/$isbn",
226         headers => { "Content-Length" => "0" }
227     };
228     my $res = $self->request($req, $session_id);
229
230     # TODO: more sophisticated response handling
231     # HTTP 200 response indicates success
232     if (defined ($res)) {
233         if ($res->{is_success}) {
234             return {
235                 xact_id => $res->{content}->{transactionId},
236                 due_date => $res->{content}->{expiration}
237             };
238         } else {
239             $logger->error("EbookAPI: renewal failed for OneClickdigital title $isbn");
240             return { error_msg => $res->{content} };
241         }
242     } else {
243         $logger->error("EbookAPI: no response received from OneClickdigital server");
244         return;
245     }
246 }
247
248 # checkin a checked-out item
249 # item id = ISBN, patron id = barcode
250 # XXX API docs indicate that a bearer token is required!
251 # DELETE //api.{domain}/v1/libraries/{libraryId}/patrons/{patronId}/checkouts/{isbn}
252 sub checkin {
253 }
254
255 sub place_hold {
256 }
257
258 sub cancel_hold {
259 }
260
261 # GET //api.{domain}/v1/libraries/{libraryId}/patrons/{patronId}/checkouts/all
262 sub get_patron_checkouts {
263     my ($self, $patron_id) = @_;
264     my $base_uri = $self->{base_uri};
265     my $library_id = $self->{library_id};
266     my $session_id = $self->{session_id};
267     my $req = {
268         method => 'GET',
269         uri    => "$base_uri/libraries/$library_id/patrons/$patron_id/checkouts/all"
270     };
271     my $res = $self->request($req, $session_id);
272
273     my $checkouts = [];
274     if (defined ($res)) {
275         $logger->info("EbookAPI: received response for OneClickdigital checkouts: " . Dumper $res);
276         foreach my $checkout (@{$res->{content}}) {
277             push @$checkouts, {
278                 xact_id => $checkout->{transactionId},
279                 title_id => $checkout->{isbn},
280                 due_date => $checkout->{expiration},
281                 download_url => $checkout->{downloadUrl},
282                 title => $checkout->{title},
283                 author => $checkout->{authors}
284             };
285         };
286         $logger->info("EbookAPI: retrieved " . scalar(@$checkouts) . " OneClickdigital checkouts for patron $patron_id");
287         $self->{checkouts} = $checkouts;
288         return $self->{checkouts};
289     } else {
290         $logger->error("EbookAPI: failed to retrieve OneClickdigital checkouts for patron $patron_id");
291         return;
292     }
293 }
294
295 # GET //api.{domain}/v1/libraries/{libraryId}/patrons/{patronId}/holds/all
296 sub get_patron_holds {
297     my ($self, $patron_id) = @_;
298     my $base_uri = $self->{base_uri};
299     my $library_id = $self->{library_id};
300     my $session_id = $self->{session_id};
301     my $req = {
302         method => 'GET',
303         uri    => "$base_uri/libraries/$library_id/patrons/$patron_id/holds/all"
304     };
305     my $res = $self->request($req, $session_id);
306
307     my $holds = [];
308     if (defined ($res)) {
309         $logger->info("EbookAPI: received response for OneClickdigital holds: " . Dumper $res);
310         foreach my $hold (@{$res->{content}}) {
311             push @$holds, {
312                 xact_id => $hold->{transactionId},
313                 title_id => $hold->{isbn},
314                 expire_date => $hold->{expiration},
315                 title => $hold->{title},
316                 author => $hold->{authors},
317                 # XXX queue position/size and pending vs ready info not available via API
318                 queue_position => '-',
319                 queue_size => '-',
320                 is_ready => 0
321             };
322         };
323         $logger->info("EbookAPI: retrieved " . scalar(@$holds) . " OneClickdigital holds for patron $patron_id");
324         $self->{holds} = $holds;
325         return $self->{holds};
326     } else {
327         $logger->error("EbookAPI: failed to retrieve OneClickdigital holds for patron $patron_id");
328         return;
329     }
330 }
331
332 1;