781 lines
21 KiB
Perl
781 lines
21 KiB
Perl
# Copyright 2015 Catalyst
|
|
|
|
package WebService::ILS::OverDrive::Patron;
|
|
|
|
use Modern::Perl;
|
|
|
|
=encoding utf-8
|
|
|
|
=head1 NAME
|
|
|
|
WebService::ILS::OverDrive::Patron - WebService::ILS module for OverDrive
|
|
circulation services
|
|
|
|
=head1 SYNOPSIS
|
|
|
|
use WebService::ILS::OverDrive::Patron;
|
|
|
|
=head1 DESCRIPTION
|
|
|
|
These services require individual user credentials.
|
|
See L<WebService::ILS INDIVIDUAL USER AUTHENTICATION AND METHODS>
|
|
|
|
See L<WebService::ILS::OverDrive>
|
|
|
|
=cut
|
|
|
|
use Carp;
|
|
use HTTP::Request::Common;
|
|
use URI::Escape;
|
|
use Data::Dumper;
|
|
|
|
use parent qw(WebService::ILS::OverDrive);
|
|
|
|
use constant CIRCULATION_API_URL => "https://patron.api.overdrive.com/";
|
|
use constant TEST_CIRCULATION_API_URL => "https://integration-patron.api.overdrive.com/";
|
|
use constant OAUTH_BASE_URL => "https://oauth.overdrive.com/";
|
|
use constant TOKEN_URL => OAUTH_BASE_URL . 'token';
|
|
use constant AUTH_URL => OAUTH_BASE_URL . 'auth';
|
|
|
|
=head1 CONSTRUCTOR
|
|
|
|
=head2 new (%params_hash or $params_hashref)
|
|
|
|
=head3 Additional constructor params:
|
|
|
|
=over 16
|
|
|
|
=item C<auth_token> => auth token as previously obtained
|
|
|
|
=back
|
|
|
|
=cut
|
|
|
|
use Class::Tiny qw(
|
|
user_id password website_id authorization_name
|
|
auth_token
|
|
), {
|
|
_circulation_api_url => sub { $_[0]->test ? TEST_CIRCULATION_API_URL : CIRCULATION_API_URL },
|
|
};
|
|
|
|
__PACKAGE__->_set_param_spec({
|
|
auth_token => { required => 0 },
|
|
});
|
|
|
|
=head1 INDIVIDUAL USER AUTHENTICATION METHODS
|
|
|
|
=head2 auth_by_user_id ($user_id, $password, $website_id, $authorization_name)
|
|
|
|
C<website_id> and C<authorization_name> (domain) are provided by OverDrive
|
|
|
|
=head3 Returns (access_token, access_token_type) or access_token
|
|
|
|
=cut
|
|
|
|
sub auth_by_user_id {
|
|
my $self = shift;
|
|
my $user_id = shift or croak "No user id";
|
|
my $password = shift; # can be blank
|
|
my $website_id = shift or croak "No website id";
|
|
my $authorization_name = shift or croak "No authorization name";
|
|
|
|
my $request = $self->_make_access_token_by_user_id_request($user_id, $password, $website_id, $authorization_name);
|
|
$self->_request_access_token($request);
|
|
|
|
$self->user_id($user_id);
|
|
$self->password($password);
|
|
$self->website_id($website_id);
|
|
$self->authorization_name($authorization_name);
|
|
return wantarray ? ($self->access_token, $self->access_token_type) : $self->access_token;
|
|
}
|
|
|
|
sub _make_access_token_by_user_id_request {
|
|
my $self = shift;
|
|
my $user_id = shift or croak "No user id";
|
|
my $password = shift; # can be blank
|
|
my $website_id = shift or croak "No website id";
|
|
my $authorization_name = shift or croak "No authorization name";
|
|
|
|
my %params = (
|
|
grant_type => 'password',
|
|
username => $user_id,
|
|
scope => "websiteid:".$website_id." authorizationname:".$authorization_name,
|
|
);
|
|
if ($password) {
|
|
$params{password} = $password;
|
|
} else {
|
|
$params{password} = "[ignore]";
|
|
$params{password_required} = "false";
|
|
}
|
|
return HTTP::Request::Common::POST( 'https://oauth-patron.overdrive.com/patrontoken', \%params );
|
|
}
|
|
|
|
=head2 Authentication at OverDrive - Granted or "3-Legged" Authorization
|
|
|
|
With OverDrive there's an extra step - an auth code is returned to the
|
|
redirect back handler that needs to make an API call to convert it into
|
|
a auth token.
|
|
|
|
An example:
|
|
|
|
my $overdrive = WebService::ILS::OverDrive::Patron({
|
|
client_id => $client_id,
|
|
client_secret => $client_secret,
|
|
library_id => $library_id,
|
|
});
|
|
my $redirect_url = $overdrive->auth_url("https://myapp.com/overdrive-auth");
|
|
$response->redirect($redirect_url);
|
|
...
|
|
/overdrive-auth handler:
|
|
my $auth_code = $req->param( $overdrive->auth_code_param_name )
|
|
or some_error_handling(), return;
|
|
# my $state = $req->param( $overdrive->state_token_param_name )...
|
|
local $@;
|
|
eval { $overdrive->auth_by_code( $auth_code ) };
|
|
if ($@) { some_error_handling(); return; }
|
|
$session{overdrive_access_token} = $access_token;
|
|
$session{overdrive_access_token_type} = $access_token_type;
|
|
$session{overdrive_auth_token} = $auth_token;
|
|
...
|
|
Somewhere else in your app:
|
|
my $ils = WebService::ILS::Provider({
|
|
client_id => $client_id,
|
|
client_secret => $client_secret,
|
|
access_token => $session{overdrive_access_token},
|
|
access_token_type => $session{overdrive_access_token_type},
|
|
auth_token = $session{overdrive_auth_token}
|
|
});
|
|
my $checkouts = $overdrive->checkouts;
|
|
|
|
=head2 auth_url ($redirect_uri, $state_token)
|
|
|
|
=head3 Input params:
|
|
|
|
=over 18
|
|
|
|
=item C<redirect_uri> => return url which will handle redirect back after auth
|
|
|
|
=item C<state_token> => a token that is returned back unchanged;
|
|
|
|
for additional security; not required
|
|
|
|
=back
|
|
|
|
=cut
|
|
|
|
sub auth_url {
|
|
my $self = shift;
|
|
my $redirect_uri = shift or croak "Redirect URI not specified";
|
|
my $state_token = shift;
|
|
|
|
my $library_id = $self->library_id or croak "No Library Id";
|
|
|
|
return sprintf AUTH_URL .
|
|
"?client_id=%s" .
|
|
"&redirect_uri=%s" .
|
|
"&scope=%s" .
|
|
"&response_type=code" .
|
|
"&state=%s",
|
|
map uri_escape($_),
|
|
$self->client_id,
|
|
$redirect_uri,
|
|
"accountid:$library_id",
|
|
defined ($state_token) ? $state_token : ""
|
|
;
|
|
}
|
|
|
|
=head2 auth_code_param_name ()
|
|
|
|
=head2 state_token_param_name ()
|
|
|
|
=cut
|
|
|
|
use constant auth_code_param_name => "code";
|
|
use constant state_token_param_name => "code";
|
|
|
|
=head2 auth_by_code ($provider_code, $redirect_uri)
|
|
|
|
=head3 Returns (access_token, access_token_type, auth_token) or access_token
|
|
|
|
=cut
|
|
|
|
sub auth_by_code {
|
|
my $self = shift;
|
|
my $code = shift or croak "No authorization code";
|
|
my $redirect_uri = shift or croak "Redirect URI not specified";
|
|
|
|
my $auth_type = 'authorization_code';
|
|
|
|
my $request = HTTP::Request::Common::POST( TOKEN_URL, {
|
|
grant_type => 'authorization_code',
|
|
code => $code,
|
|
redirect_uri => $redirect_uri,
|
|
} );
|
|
$self->_request_access_token($request);
|
|
return wantarray ? ($self->access_token, $self->access_token_type, $self->auth_token) : $self->access_token;
|
|
}
|
|
|
|
=head2 auth_by_token ($provider_token)
|
|
|
|
=head3 Returns (access_token, access_token_type, auth_token) or access_token
|
|
|
|
=cut
|
|
|
|
sub auth_by_token {
|
|
my $self = shift;
|
|
my $auth_token = shift or croak "No authorization token";
|
|
|
|
$self->auth_token($auth_token);
|
|
my $request = $self->_make_access_token_by_auth_token_request($auth_token);
|
|
$self->_request_access_token($request);
|
|
|
|
return wantarray ? ($self->access_token, $self->access_token_type, $self->auth_token) : $self->access_token;
|
|
}
|
|
|
|
sub _make_access_token_by_auth_token_request {
|
|
my $self = shift;
|
|
my $auth_token = shift or croak "No authorization token";
|
|
|
|
return HTTP::Request::Common::POST( TOKEN_URL, {
|
|
grant_type => 'refresh_token',
|
|
refresh_token => $auth_token,
|
|
} );
|
|
}
|
|
|
|
sub make_access_token_request {
|
|
my $self = shift;
|
|
|
|
if (my $auth_token = $self->auth_token) {
|
|
return $self->_make_access_token_by_auth_token_request($auth_token);
|
|
}
|
|
elsif (my $user_id = $self->user_id) {
|
|
return $self->_make_access_token_by_user_id_request(
|
|
$user_id, $self->password, $self->website_id, $self->authorization_name
|
|
);
|
|
}
|
|
|
|
die $self->ERROR_NOT_AUTHENTICATED."\n";
|
|
}
|
|
|
|
sub _request_access_token {
|
|
my $self = shift;
|
|
my $request = shift or croak "No request";
|
|
|
|
my $data = $self->SUPER::_request_access_token($request)
|
|
or die "Unsuccessful access token request";
|
|
|
|
if (my $auth_token = $data->{refresh_token}) {
|
|
$self->auth_token($auth_token);
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
|
|
sub collection_token {
|
|
my $self = shift;
|
|
|
|
if (my $collection_token = $self->SUPER::collection_token) {
|
|
return $collection_token;
|
|
}
|
|
|
|
$self->native_patron; # sets collection_token as a side-effect
|
|
my $collection_token = $self->SUPER::collection_token
|
|
or die "Patron has no collections\n";
|
|
return $collection_token;
|
|
}
|
|
|
|
=head1 CIRCULATION METHOD SPECIFICS
|
|
|
|
Differences to general L<WebService::ILS> interface
|
|
|
|
=cut
|
|
|
|
my %PATRON_XLATE = (
|
|
checkoutLimit => "checkout_limit",
|
|
existingPatron => 'active',
|
|
patronId => 'id',
|
|
holdLimit => 'hold_limit',
|
|
);
|
|
sub patron {
|
|
my $self = shift;
|
|
return $self->_result_xlate($self->native_patron, \%PATRON_XLATE);
|
|
}
|
|
|
|
my %HOLDS_XLATE = (
|
|
totalItems => 'total',
|
|
);
|
|
my %HOLDS_ITEM_XLATE = (
|
|
reserveId => 'id',
|
|
holdPlacedDate => 'placed_datetime',
|
|
holdListPosition => 'queue_position',
|
|
);
|
|
sub holds {
|
|
my $self = shift;
|
|
|
|
my $holds = $self->native_holds;
|
|
my $items = delete ($holds->{holds}) || [];
|
|
|
|
my $res = $self->_result_xlate($holds, \%HOLDS_XLATE);
|
|
$res->{items} = [
|
|
map {
|
|
my $item = $self->_result_xlate($_, \%HOLDS_ITEM_XLATE);
|
|
my $item_id = $item->{id};
|
|
my $metadata = $self->item_metadata($item_id);
|
|
my $i = {%$item, %$metadata}; # we need my $i, don't ask me why...
|
|
} @$items
|
|
];
|
|
return $res;
|
|
}
|
|
|
|
=head2 place_hold ($item_id, $notification_email_address, $auto_checkout)
|
|
|
|
C<$notification_email_address> and C<$auto_checkout> are optional.
|
|
C<$auto_checkout> defaults to false.
|
|
|
|
=head3 Returns holds item record
|
|
|
|
It is preferred that the C<$notification_email_address> is specified.
|
|
|
|
If C<$auto_checkout> is set to true, the item will be checked out as soon as
|
|
it becomes available.
|
|
|
|
=cut
|
|
|
|
sub place_hold {
|
|
my $self = shift;
|
|
|
|
my $hold = $self->native_place_hold(@_) or return;
|
|
my $res = $self->_result_xlate($hold, \%HOLDS_ITEM_XLATE);
|
|
$res->{total} = $hold->{numberOfHolds};
|
|
return $res;
|
|
}
|
|
|
|
# sub suspend_hold { - not really useful
|
|
|
|
sub remove_hold {
|
|
my $self = shift;
|
|
my $item_id = shift or croak "No item id";
|
|
|
|
my $url = $self->circulation_action_url("/holds/$item_id");
|
|
return $self->with_delete_request(
|
|
\&_basic_callback,
|
|
sub {
|
|
my ($data) = @_;
|
|
return 1 if $data->{errorCode} eq "PatronDoesntHaveTitleOnHold";
|
|
my $error = ($data->{message} || $data->{errorCode})."\n";
|
|
die $error;
|
|
},
|
|
$url
|
|
);
|
|
}
|
|
|
|
=head2 checkouts ()
|
|
|
|
For formats see C<checkout_formats()> below
|
|
|
|
=cut
|
|
|
|
my %CHECKOUTS_XLATE = (
|
|
totalItems => 'total',
|
|
totalCheckouts => 'total_format',
|
|
);
|
|
sub checkouts {
|
|
my $self = shift;
|
|
|
|
my $checkouts = $self->native_checkouts;
|
|
my $items = delete ($checkouts->{checkouts}) || [];
|
|
|
|
my $res = $self->_result_xlate($checkouts, \%CHECKOUTS_XLATE);
|
|
$res->{items} = [
|
|
map {
|
|
my $item = $self->_checkout_item_xlate($_);
|
|
my $item_id = $item->{id};
|
|
my $formats = delete ($_->{formats});
|
|
my $actions = delete ($_->{actions});
|
|
my $metadata = $self->item_metadata($item_id);
|
|
if ($formats) {
|
|
$formats = $self->_formats_xlate($item_id, $formats);
|
|
}
|
|
else {
|
|
$formats = {};
|
|
}
|
|
if ($actions) {
|
|
if (my $format_action = $actions->{format}) {
|
|
foreach (@{$format_action->{fields}}) {
|
|
next unless $_->{name} eq "formatType";
|
|
|
|
foreach my $format (@{$_->{options}}) {
|
|
$formats->{$format} = undef unless exists $formats->{$format};
|
|
}
|
|
last;
|
|
}
|
|
}
|
|
}
|
|
my $i = {%$item, %$metadata, formats => $formats}; # we need my $i, don't ask me why...
|
|
} @$items
|
|
];
|
|
return $res;
|
|
}
|
|
|
|
my %CHECKOUT_ITEM_XLATE = (
|
|
reserveId => 'id',
|
|
checkoutDate => 'checkout_datetime',
|
|
expires => 'expires',
|
|
);
|
|
sub _checkout_item_xlate {
|
|
my $self = shift;
|
|
my $item = shift;
|
|
|
|
my $i = $self->_result_xlate($item, \%CHECKOUT_ITEM_XLATE);
|
|
if ($item->{isFormatLockedIn}) {
|
|
my $formats = $item->{formats} or die "Item $item->{reserveId}: Format locked in, but no formats returned\n";
|
|
$i->{format} = $formats->[0]{formatType};
|
|
}
|
|
return $i;
|
|
}
|
|
|
|
=head2 checkout ($item_id, $format, $allow_multiple_format_checkouts)
|
|
|
|
C<$format> and C<$allow_multiple_format_checkouts> are optional.
|
|
C<$allow_multiple_format_checkouts> defaults to false.
|
|
|
|
=head3 Returns checkout item record
|
|
|
|
An item can be available in multiple formats. Checkout is complete only
|
|
when the format is specified.
|
|
|
|
Checkout can be actioned without format being specified. In that case an
|
|
early return can be actioned. To complete checkout format must be locked
|
|
later (see L<lock_format()> below). That would be the case with
|
|
L<place_hold()> with C<$auto_checkout> set to true. Once format is locked,
|
|
an early return is not possible.
|
|
|
|
If C<$allow_multiple_format_checkouts> flag is set to true, multiple formats
|
|
of the same item can be acioned. If it is false (default) and the item was
|
|
already checked out, the checked out item record will be returned regardless
|
|
of the format.
|
|
|
|
Checkout record will have an extra field C<format> if format is locked in.
|
|
|
|
=cut
|
|
|
|
sub checkout {
|
|
my $self = shift;
|
|
|
|
my $checkout = $self->native_checkout(@_) or return;
|
|
return $self->_checkout_item_xlate($checkout);
|
|
}
|
|
|
|
=head2 checkout_formats ($item_id)
|
|
|
|
=head3 Returns a hashref of available title formats and immediate availability
|
|
|
|
{ format => available, ... }
|
|
|
|
If format is not immediately available it must be locked first
|
|
|
|
=cut
|
|
|
|
sub checkout_formats {
|
|
my $self = shift;
|
|
my $id = shift or croak "No item id";
|
|
|
|
my $formats = $self->native_checkout_formats($id) or return;
|
|
$formats = $formats->{'formats'} or return;
|
|
return $self->_formats_xlate($id, $formats);
|
|
}
|
|
|
|
sub _formats_xlate {
|
|
my $self = shift;
|
|
my $id = shift or croak "No item id";
|
|
my $formats = shift or croak "No formats";
|
|
|
|
my %ret;
|
|
my $id_uc = uc $id;
|
|
foreach (@$formats) {
|
|
die "Non-matching item id\nExpected $id\nGot $_->{reserveId}" unless uc($_->{reserveId}) eq $id_uc;
|
|
my $format = $_->{formatType};
|
|
my $available;
|
|
if (my $lt = $_->{linkTemplates}) {
|
|
$available = grep /^downloadLink/, keys %$lt;
|
|
}
|
|
$ret{$format} = $available;
|
|
}
|
|
return \%ret;
|
|
}
|
|
|
|
sub is_lockable {
|
|
my $self = shift;
|
|
my $checkout_formats = shift or croak "No checkout formats";
|
|
while (my ($format, $available) = each %$checkout_formats) {
|
|
return 1 unless $available;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
=head2 lock_format ($item_id, $format)
|
|
|
|
=head3 Returns locked format (should be the same as the input value)
|
|
|
|
=cut
|
|
|
|
sub lock_format {
|
|
my $self = shift;
|
|
my $item_id = shift or croak "No item id";
|
|
my $format = shift or croak "No format";
|
|
|
|
my $lock = $self->native_lock_format($item_id, $format) or return;
|
|
die "Non-matching item id\nExpected $item_id\nGot $lock->{reserveId}" unless uc($lock->{reserveId}) eq uc($item_id);
|
|
return $lock->{formatType};
|
|
}
|
|
|
|
=head2 checkout_download_url ($item_id, $format, $error_url, $success_url)
|
|
|
|
=head3 Returns OverDrive download url
|
|
|
|
Checked out items must be downloaded by users on the OverDrive site.
|
|
This method returns the url where the user should be sent to (redirected).
|
|
Once the download is complete, user will be redirected back to
|
|
C<$error_url> in case of an error, otherwise to optional C<$success_url>
|
|
if specified.
|
|
|
|
See L<https://developer.overdrive.com/apis/download>
|
|
|
|
=cut
|
|
|
|
sub checkout_download_url {
|
|
my $self = shift;
|
|
my $item_id = shift or croak "No item id";
|
|
my $format = shift or croak "No format";
|
|
my $error_url = shift or die "No error url";
|
|
my $success_url = shift;
|
|
|
|
$error_url = uri_escape($error_url);
|
|
$success_url = $success_url ? uri_escape($success_url) : '';
|
|
my $url = $self->circulation_action_url("/checkouts/$item_id/formats/$format/downloadlink?errorurl=$error_url&successurl=$success_url");
|
|
my $response_data = $self->get_response($url);
|
|
my $download_url =
|
|
_extract_link($response_data, 'contentLink') ||
|
|
_extract_link($response_data, 'contentlink')
|
|
or die "Cannot get download url\n".Dumper($response_data);
|
|
return $download_url;
|
|
}
|
|
|
|
sub return {
|
|
my $self = shift;
|
|
my $item_id = shift or croak "No item id";
|
|
|
|
my $url = $self->circulation_action_url("/checkouts/$item_id");
|
|
return $self->with_delete_request(
|
|
\&_basic_callback,
|
|
sub {
|
|
my ($data) = @_;
|
|
return 1 if $data->{errorCode} eq "PatronDoesntHaveTitleCheckedOut";
|
|
my $error = ($data->{message} || $data->{errorCode})."\n";
|
|
die $error;
|
|
},
|
|
$url
|
|
);
|
|
}
|
|
|
|
=head1 NATIVE METHODS
|
|
|
|
=head2 native_patron ()
|
|
|
|
See L<https://developer.overdrive.com/apis/patron-information>
|
|
|
|
=cut
|
|
|
|
sub native_patron {
|
|
my $self = shift;
|
|
|
|
my $url = $self->circulation_action_url("");
|
|
my $patron = $self->get_response($url) or return;
|
|
if (my $collection_token = $patron->{collectionToken}) {
|
|
$self->SUPER::collection_token( $collection_token);
|
|
}
|
|
return $patron;
|
|
}
|
|
|
|
=head2 native_holds ()
|
|
|
|
=head2 native_place_hold ($item_id, $notification_email_address, $auto_checkout)
|
|
|
|
See L<https://developer.overdrive.com/apis/holds>
|
|
|
|
=cut
|
|
|
|
sub native_holds {
|
|
my $self = shift;
|
|
my $url = $self->circulation_action_url("/holds");
|
|
return $self->get_response($url);
|
|
}
|
|
|
|
sub native_place_hold {
|
|
my $self = shift;
|
|
my $item_id = shift or croak "No item id";
|
|
my $email = shift;
|
|
my $auto_checkout = shift;
|
|
|
|
my @fields = ( {name => "reserveId", value => $item_id } );
|
|
push @fields, {name => "autoCheckout", value => "true"} if $auto_checkout;
|
|
if ($email) {
|
|
push @fields, {name => "emailAddress", value => $email};
|
|
} else {
|
|
push @fields, {name => "ignoreHoldEmail", value => "true"};
|
|
}
|
|
|
|
my $url = $self->circulation_action_url("/holds");
|
|
return $self->with_json_request(
|
|
\&_basic_callback,
|
|
sub {
|
|
my ($data) = @_;
|
|
if ($data->{errorCode} eq "AlreadyOnWaitList") {
|
|
if (my $holds = $self->native_holds) {
|
|
my $item_id_uc = uc $item_id;
|
|
foreach (@{ $holds->{holds} || [] }) {
|
|
if ( uc($_->{reserveId}) eq $item_id_uc ) {
|
|
$_->{numberOfHolds} = $holds->{totalItems};
|
|
return $_;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
my $error = ($data->{message} || $data->{errorCode})."\n";
|
|
die $error;
|
|
},
|
|
$url,
|
|
{fields => \@fields}
|
|
);
|
|
}
|
|
|
|
=head2 native_checkouts ()
|
|
|
|
=head2 native_checkout_info ($item_id)
|
|
|
|
=head2 native_checkout ($item_id, $format, $allow_multiple_format_checkouts)
|
|
|
|
=head2 native_checkout_formats ($item_id)
|
|
|
|
=head2 native_lock_format ($item_id, $format)
|
|
|
|
See L<https://developer.overdrive.com/apis/checkouts>
|
|
|
|
=cut
|
|
|
|
sub native_checkouts {
|
|
my $self = shift;
|
|
|
|
my $url = $self->circulation_action_url("/checkouts");
|
|
return $self->get_response($url);
|
|
}
|
|
|
|
sub native_checkout_info {
|
|
my $self = shift;
|
|
my $id = shift or croak "No item id";
|
|
|
|
my $url = $self->circulation_action_url("/checkouts/$id");
|
|
return $self->get_response($url);
|
|
}
|
|
|
|
sub native_checkout_formats {
|
|
my $self = shift;
|
|
my $id = shift or croak "No item id";
|
|
|
|
my $url = $self->circulation_action_url("/checkouts/$id/formats");
|
|
return $self->get_response($url);
|
|
}
|
|
|
|
sub native_checkout {
|
|
my $self = shift;
|
|
my $item_id = shift or croak "No item id";
|
|
my $format = shift;
|
|
my $allow_multi = shift;
|
|
|
|
if (my $checkouts = $self->native_checkouts) {
|
|
my $item_id_uc = uc $item_id;
|
|
foreach (@{ $checkouts->{checkouts} || [] }) {
|
|
if ( uc($_->{reserveId}) eq $item_id_uc ) {
|
|
if ($format) {
|
|
if ($_->{isFormatLockedIn}) {
|
|
return $_ if lc($_->{formats}[0]{formatType}) eq lc($format);
|
|
die "Item $item_id has already been locked for different format '$_->{formats}[0]{formatType}'\n"
|
|
unless $allow_multi;
|
|
}
|
|
# else { $self->native_lock_format()? }
|
|
}
|
|
# else { die if !$allow_multi ? }
|
|
return $_;
|
|
}
|
|
}
|
|
}
|
|
|
|
my $url = $self->circulation_action_url("/checkouts");
|
|
return $self->with_json_request(
|
|
\&_basic_callback,
|
|
undef,
|
|
$url,
|
|
{fields => _build_checkout_fields($item_id, $format)}
|
|
);
|
|
}
|
|
|
|
sub native_lock_format {
|
|
my $self = shift;
|
|
my $item_id = shift or croak "No item id";
|
|
my $format = shift or croak "No format";
|
|
|
|
my $url = $self->circulation_action_url("/checkouts/$item_id/formats");
|
|
return $self->with_json_request(
|
|
\&_basic_callback,
|
|
sub {
|
|
my ($data) = @_;
|
|
die "$format ".($data->{message} || $data->{errorCode})."\n";
|
|
},
|
|
$url,
|
|
{fields => _build_checkout_fields($item_id, $format)}
|
|
);
|
|
}
|
|
|
|
sub _build_checkout_fields {
|
|
my ($id, $format) = @_;
|
|
my @fields = ( {name => "reserveId", value => $id } );
|
|
push @fields, {name => "formatType", value => $format} if $format;
|
|
return \@fields;
|
|
}
|
|
|
|
# Circulation helpers
|
|
|
|
sub circulation_action_url {
|
|
my $self = shift;
|
|
my $action = shift;
|
|
return $self->_circulation_api_url.$self->API_VERSION."/patrons/me$action";
|
|
}
|
|
|
|
# API helpers
|
|
|
|
sub _extract_link {
|
|
my ($data, $link) = @_;
|
|
return $data->{links}{$link}->{href};
|
|
}
|
|
|
|
sub _basic_callback { return $_[0]; }
|
|
|
|
1;
|
|
|
|
__END__
|
|
|
|
=head1 LICENSE
|
|
|
|
Copyright (C) Catalyst IT NZ Ltd
|
|
Copyright (C) Bywater Solutions
|
|
|
|
This library is free software; you can redistribute it and/or modify
|
|
it under the same terms as Perl itself.
|
|
|
|
=head1 AUTHOR
|
|
|
|
Srdjan Janković E<lt>srdjan@catalyst.net.nzE<gt>
|
|
|
|
=cut
|