Skip to content

Commit

Permalink
self-checkout: provide ad-hoc endpoints
Browse files Browse the repository at this point in the history
* define new ad-hoc search and checkout endpoints, to be able to have
  better control on contraints and input and output payloads
* use delivery methods to store when the checkout is a self-checkout
  • Loading branch information
ntarocco committed Oct 29, 2024
1 parent 05797ae commit 0a6c421
Show file tree
Hide file tree
Showing 14 changed files with 611 additions and 142 deletions.
146 changes: 120 additions & 26 deletions invenio_app_ils/circulation/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,15 @@
get_all_expiring_or_overdue_loans_by_patron_pid,
)
from invenio_app_ils.errors import (
DocumentOverbookedError,
IlsException,
InvalidLoanExtendError,
InvalidParameterError,
ItemCannotCirculateError,
ItemHasActiveLoanError,
ItemNotFoundError,
MissingRequiredParameterError,
MultipleItemsBarcodeFoundError,
PatronHasLoanOnDocumentError,
PatronHasLoanOnItemError,
PatronHasRequestOnDocumentError,
Expand Down Expand Up @@ -119,7 +124,7 @@ def request_loan(
patron_pid,
transaction_location_pid,
transaction_user_pid=None,
**kwargs
**kwargs,
):
"""Create a new loan and trigger the first transition to PENDING."""
loan_cls = current_circulation.loan_record_cls
Expand Down Expand Up @@ -170,13 +175,51 @@ def patron_has_active_loan_on_item(patron_pid, item_pid):
return search_result.hits.total.value > 0


def _checkout_loan(
item_pid,
patron_pid,
transaction_location_pid,
transaction_user_pid=None,
trigger="checkout",
**kwargs,
):
"""Checkout a loan."""
transaction_user_pid = transaction_user_pid or str(current_user.id)
loan_cls = current_circulation.loan_record_cls
# create a new loan
record_uuid = uuid.uuid4()
new_loan = dict(
patron_pid=patron_pid,
transaction_location_pid=transaction_location_pid,
transaction_user_pid=transaction_user_pid,
)

# check if there is an existing request
loan = patron_has_request_on_document(patron_pid, kwargs.get("document_pid"))
if loan:
loan = loan_cls.get_record_by_pid(loan.pid)
pid = IlsCirculationLoanIdProvider.get(loan["pid"]).pid
loan.update(new_loan)
else:
pid = ils_circulation_loan_pid_minter(record_uuid, data=new_loan)
loan = loan_cls.create(data=new_loan, id_=record_uuid)

params = deepcopy(loan)
params.update(item_pid=item_pid, **kwargs)

loan = current_circulation.circulation.trigger(
loan, **dict(params, trigger=trigger)
)
return pid, loan


def checkout_loan(
item_pid,
patron_pid,
transaction_location_pid,
transaction_user_pid=None,
force=False,
**kwargs
**kwargs,
):
"""Create a new loan and trigger the first transition to ITEM_ON_LOAN.
Expand All @@ -191,7 +234,7 @@ def checkout_loan(
the checkout. If False, the checkout will fail when the item cannot
circulate.
"""
loan_cls = current_circulation.loan_record_cls

if patron_has_active_loan_on_item(patron_pid=patron_pid, item_pid=item_pid):
raise PatronHasLoanOnItemError(patron_pid, item_pid)
optional_delivery = kwargs.get("delivery")
Expand All @@ -201,35 +244,86 @@ def checkout_loan(
if force:
_set_item_to_can_circulate(item_pid)

transaction_user_pid = transaction_user_pid or str(current_user.id)

# create a new loan
record_uuid = uuid.uuid4()
new_loan = dict(
patron_pid=patron_pid,
transaction_location_pid=transaction_location_pid,
return _checkout_loan(
item_pid,
patron_pid,
transaction_location_pid,
transaction_user_pid=transaction_user_pid,
**kwargs,
)

# check if there is an existing request
loan = patron_has_request_on_document(patron_pid, kwargs.get("document_pid"))
if loan:
loan = loan_cls.get_record_by_pid(loan.pid)
pid = IlsCirculationLoanIdProvider.get(loan["pid"]).pid
loan.update(new_loan)
else:
pid = ils_circulation_loan_pid_minter(record_uuid, data=new_loan)
loan = loan_cls.create(data=new_loan, id_=record_uuid)

params = deepcopy(loan)
params.update(item_pid=item_pid, **kwargs)
def _ensure_item_loanable_via_self_checkout(item_pid):
"""Self-checkout: return loanable item or raise when not loanable.
# trigger the transition to request
loan = current_circulation.circulation.trigger(
loan, **dict(params, trigger="checkout")
Implements the self-checkout rules to loan an item.
"""
item = current_app_ils.item_record_cls.get_record_by_pid(item_pid)
item_dict = item.replace_refs()

if item_dict["status"] != "CAN_CIRCULATE":
raise ItemCannotCirculateError()

circulation_state = item_dict["circulation"].get("state")
has_active_loan = (
circulation_state and circulation_state in CIRCULATION_STATES_LOAN_ACTIVE
)
if has_active_loan:
raise ItemHasActiveLoanError(loan_pid=item_dict["circulation"]["loan_pid"])

return pid, loan
document = current_app_ils.document_record_cls.get_record_by_pid(
item_dict["document_pid"]
)
document_dict = document.replace_refs()
if document_dict["circulation"].get("overbooked", False):
raise DocumentOverbookedError(
f"Cannot self-checkout the overbooked document {item_dict['document_pid']}"
)

return item


def self_checkout_get_item_by_barcode(barcode):
"""Search for an item by barcode.
:param barcode: the barcode of the item to search for
:return item: the item that was found, or raise in case of errors
"""
item_search = current_app_ils.item_search_cls()
items = item_search.search_by_barcode(barcode).execute()
if items.hits.total.value == 0:
raise ItemNotFoundError(barcode=barcode)
if items.hits.total.value > 1:
raise MultipleItemsBarcodeFoundError(barcode)

item_pid = items.hits[0].pid
item = _ensure_item_loanable_via_self_checkout(item_pid)
return item_pid, item


def self_checkout(
item_pid, patron_pid, transaction_location_pid, transaction_user_pid=None, **kwargs
):
"""Perform self-checkout.
:param item_pid: a dict containing `value` and `type` fields to
uniquely identify the item.
:param patron_pid: the PID value of the patron
:param transaction_location_pid: the PID value of the location where the
checkout is performed
:param transaction_user_pid: the PID value of the user that performed the
checkout
"""
_ensure_item_loanable_via_self_checkout(item_pid["value"])
return _checkout_loan(
item_pid,
patron_pid,
transaction_location_pid,
transaction_user_pid=transaction_user_pid,
trigger="self_checkout",
delivery=dict(method="SELF-CHECKOUT"),
**kwargs,
)


def bulk_extend_loans(patron_pid, **kwargs):
Expand All @@ -253,7 +347,7 @@ def bulk_extend_loans(patron_pid, **kwargs):
params,
trigger="extend",
transition_kwargs=dict(send_notification=False),
)
),
)
extended_loans.append(extended_loan)
except (CirculationException, InvalidLoanExtendError):
Expand Down
16 changes: 14 additions & 2 deletions invenio_app_ils/circulation/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@
PatronOwnerPermission,
authenticated_user_permission,
backoffice_permission,
loan_checkout_permission,
loan_extend_circulation_permission,
patron_owner_permission,
superuser_permission,
Expand Down Expand Up @@ -84,6 +83,7 @@
ILS_CIRCULATION_DELIVERY_METHODS = {
"PICKUP": "Pick it up at the library desk",
"DELIVERY": "Have it delivered to my office",
"SELF-CHECKOUT": "Self-checkout",
}

# Notification message creator for loan notifications
Expand Down Expand Up @@ -162,7 +162,13 @@
dest="ITEM_ON_LOAN",
trigger="checkout",
transition=ILSToItemOnLoan,
permission_factory=loan_checkout_permission,
permission_factory=backoffice_permission,
),
dict(
dest="ITEM_ON_LOAN",
trigger="self_checkout",
transition=ILSToItemOnLoan,
permission_factory=authenticated_user_permission,
),
],
"PENDING": [
Expand All @@ -172,6 +178,12 @@
transition=ILSToItemOnLoan,
permission_factory=backoffice_permission,
),
dict(
dest="ITEM_ON_LOAN",
trigger="self_checkout",
transition=ILSToItemOnLoan,
permission_factory=authenticated_user_permission,
),
dict(
dest="CANCELLED",
trigger="cancel",
Expand Down
2 changes: 2 additions & 0 deletions invenio_app_ils/circulation/loaders/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@
from .schemas.json.bulk_extend import BulkExtendLoansSchemaV1
from .schemas.json.loan_checkout import LoanCheckoutSchemaV1
from .schemas.json.loan_request import LoanRequestSchemaV1
from .schemas.json.loan_self_checkout import LoanSelfCheckoutSchemaV1
from .schemas.json.loan_update_dates import LoanUpdateDatesSchemaV1

loan_request_loader = ils_marshmallow_loader(LoanRequestSchemaV1)
loan_checkout_loader = ils_marshmallow_loader(LoanCheckoutSchemaV1)
loan_self_checkout_loader = ils_marshmallow_loader(LoanSelfCheckoutSchemaV1)
loan_update_dates_loader = ils_marshmallow_loader(LoanUpdateDatesSchemaV1)
loans_bulk_update_loader = ils_marshmallow_loader(BulkExtendLoansSchemaV1)
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2019 CERN.
#
# invenio-app-ils is free software; you can redistribute it and/or modify
# it under the terms of the MIT License; see LICENSE file for more details.

"""Invenio App ILS circulation Loan Checkout loader JSON schema."""

from invenio_circulation.records.loaders.schemas.json import LoanItemPIDSchemaV1
from marshmallow import fields

from .base import LoanBaseSchemaV1


class LoanSelfCheckoutSchemaV1(LoanBaseSchemaV1):
"""Loan self-checkout schema."""

item_pid = fields.Nested(LoanItemPIDSchemaV1, required=True)
1 change: 1 addition & 0 deletions invenio_app_ils/circulation/notifications/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class NotificationLoanMsg(NotificationMsg):
request="request.html",
request_no_items="request_no_items.html",
checkout="checkout.html",
self_checkout="self_checkout.html",
checkin="checkin.html",
extend="extend.html",
cancel="cancel.html",
Expand Down
1 change: 1 addition & 0 deletions invenio_app_ils/circulation/serializers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
# under the terms of the MIT License; see LICENSE file for more details.

"""Loan serializers."""

from invenio_records_rest.serializers.response import search_responsify

from invenio_app_ils.records.schemas.json import ILSRecordSchemaJSONV1
Expand Down
1 change: 1 addition & 0 deletions invenio_app_ils/circulation/serializers/response.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
# under the terms of the MIT License; see LICENSE file for more details.

"""Response serializers for circulation module."""

import json

from flask import current_app
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{% block title %}
InvenioILS: loan started for "{{ document.title|safe }}"
{% endblock %}

{% block body_plain %}
Dear {{ patron.name }},

your loan for "{{ document.full_title }}" <{{ spa_routes.HOST }}{{ spa_routes.PATHS['literature']|format(pid=document.pid) }}> has started.

The due date is {{ loan.end_date }}.
{% endblock %}

{% block body_html %}
Dear {{ patron.name }}, <br/><br/>

your loan for <a href="{{ spa_routes.HOST }}{{ spa_routes.PATHS['literature']|format(pid=document.pid) }}">"{{ document.full_title }}"</a> has <b>started</b>. <br/><br/>

<b>The due date is {{ loan.end_date }}</b>.<br/>
{% endblock %}
Loading

0 comments on commit 0a6c421

Please sign in to comment.