Skip to content

Commit

Permalink
self-checkout: remove handling errors from the UI
Browse files Browse the repository at this point in the history
* add UI for displaying self-checkout delivery method for the loans on the backoffice
* closes CERNDocumentServer/cds-ils#927
  • Loading branch information
ntarocco committed Nov 18, 2024
1 parent 45d5c32 commit 4715615
Show file tree
Hide file tree
Showing 10 changed files with 82 additions and 95 deletions.
26 changes: 26 additions & 0 deletions src/lib/api/loans/loan.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { serializer } from './serializer';

const apiPaths = {
checkout: '/circulation/loans/checkout',
selfCheckout: '/circulation/loans/self-checkout',
notificationOverdue: '/circulation/loans/:loanPid/notification-overdue',
item: '/circulation/loans/:loanPid',
list: '/circulation/loans/',
Expand Down Expand Up @@ -114,6 +115,29 @@ const doCheckout = async (
return response;
};

const doSelfCheckoutSearchItem = async (barcode) => {
const response = await http.get(
`${apiPaths.selfCheckout}?barcode=${barcode}`
);
response.data = serializer.fromJSON(response.data);
return response;
};

const doSelfCheckout = async (documentPid, itemPid, patronPid) => {
const currentUser = sessionManager.user;
const payload = {
document_pid: documentPid,
item_pid: itemPid,
patron_pid: patronPid,
transaction_location_pid: `${currentUser.locationPid}`,
transaction_user_pid: `${currentUser.id}`,
};

const response = await http.post(apiPaths.selfCheckout, payload);
response.data = serializer.fromJSON(response.data);
return response;
};

const assignItemToLoan = async (itemPid, loanPid) => {
const path = generatePath(apiPaths.replaceItem, { loanPid: loanPid });
const payload = { item_pid: itemPid };
Expand Down Expand Up @@ -332,6 +356,8 @@ export const loanApi = {
doAction: doAction,
doRequest: doRequest,
doCheckout: doCheckout,
doSelfCheckout: doSelfCheckout,
doSelfCheckoutSearchItem: doSelfCheckoutSearchItem,
sendOverdueLoansNotificationReminder: sendOverdueLoansNotificationReminder,
serializer: serializer,
updateDates: updateDates,
Expand Down
6 changes: 6 additions & 0 deletions src/lib/config/defaultConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,12 @@ export const RECORDS_CONFIG = {
iconClass: 'dolly',
},
},
deliveryMethodSelfCheckout: {
'SELF-CHECKOUT': {
text: 'SELF-CHECKOUT',
iconClass: 'shopping basket',
},
},
extensionsMaxCount: 3,
loanWillExpireDays: 7,
loanActiveStates: [
Expand Down
16 changes: 15 additions & 1 deletion src/lib/modules/Loan/backoffice/LoanList/LoanListEntry.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export class LoanListEntry extends Component {
delivery && loan.metadata.state === 'PENDING'
? invenioConfig.CIRCULATION.deliveryMethods[delivery]
: '';
const isSelfCheckout = delivery === 'SELF-CHECKOUT';
return (
<Item>
<Item.Content>
Expand Down Expand Up @@ -79,13 +80,26 @@ export class LoanListEntry extends Component {
</List>
</Grid.Column>
<Grid.Column width={2} textAlign="center">
{deliveryMethod && (
{deliveryMethod ? (
<>
{delivery}{' '}
{deliveryMethod.iconClass && (
<Icon className={deliveryMethod.iconClass} />
)}
</>
) : (
isSelfCheckout && (
<>
{delivery}{' '}
<Icon
className={
invenioConfig.CIRCULATION.deliveryMethodSelfCheckout[
delivery
].iconClass
}
/>
</>
)
)}
</Grid.Column>
<Grid.Column computer={3} largeScreen={3}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ export class LoanMetadata extends Component {
const delivery = _get(_delivery, 'method');
if (delivery) {
const deliveryMethod =
invenioConfig.CIRCULATION.deliveryMethods[delivery];
invenioConfig.CIRCULATION.deliveryMethods[delivery] ||
invenioConfig.CIRCULATION.deliveryMethodSelfCheckout[delivery];

return (
<>
{deliveryMethod.text}{' '}
Expand Down
52 changes: 1 addition & 51 deletions src/lib/pages/frontsite/SelfCheckout/SelfCheckout.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,6 @@ import {
import { BarcodeScanner } from '@components/BarcodeScanner';
import { SelfCheckoutModal } from './SelfCheckoutModal';
import { ManualCheckout } from './ManualCheckout';
import { invenioConfig } from '@config';
import _isEmpty from 'lodash/isEmpty';
import _find from 'lodash/find';

class SelfCheckout extends React.Component {
constructor(props) {
Expand All @@ -41,50 +38,10 @@ class SelfCheckout extends React.Component {
this.setState({ barcode: detectedBarcode });
const { selfCheckOutSearch } = this.props;
await selfCheckOutSearch(detectedBarcode);

// open modal if item is loanable
const shouldShowModal = this.isItemLoanable(detectedBarcode);
if (shouldShowModal) {
this.toggleModal(true);
} else {
this.toggleModal(false);
}
this.toggleModal(true);
}
};

itemStatus = (item) => ({
canCirculate: () =>
invenioConfig.ITEMS.canCirculateStatuses.includes(item.metadata.status),
isOnShelf: () => !item.metadata.circulation.state, // on shelf if circulation.state doesn't exist
});

isItemLoanable = (itemBarcode) => {
const { user, item, notifyResultMessage } = this.props;
var resultMessage = `Book with barcode ${itemBarcode} not found.`;

if (!_isEmpty(item)) {
if (this.itemStatus(item).canCirculate()) {
if (this.itemStatus(item).isOnShelf()) {
return true;
} else {
if (item.metadata.circulation.patron_pid === user.id.toString()) {
resultMessage = `You already loaned this book with barcode: ${itemBarcode}!`;
} else {
resultMessage = `Book with barcode: ${itemBarcode} is currently on loan!`;
}
}
} else {
const status = _find(invenioConfig.ITEMS.statuses, {
value: item.metadata?.status,
});
resultMessage = `Book with barcode: ${itemBarcode} is ${status?.text}!`;
}
}

notifyResultMessage(resultMessage);
return false;
};

renderInstructions = () => {
return (
<>
Expand Down Expand Up @@ -169,13 +126,6 @@ class SelfCheckout extends React.Component {
SelfCheckout.propTypes = {
/* REDUX */
selfCheckOutSearch: PropTypes.func.isRequired,
user: PropTypes.object.isRequired,
item: PropTypes.object,
notifyResultMessage: PropTypes.func.isRequired,
};

SelfCheckout.defaultProps = {
item: null,
};

export default Overridable.component('SelfCheckout', SelfCheckout);
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,13 @@ export default class SelfCheckoutModal extends React.Component {
onClose={() => toggleModal(false)}
>
<Modal.Header>
{`You are about to checkout a book with barcode:
{`You are about to checkout the literature with barcode:
${item?.metadata.barcode}`}
</Modal.Header>
<Modal.Content>
<DocumentCard item={item} />
<ManualCheckout
label="Wrong book?"
label="Wrong literature?"
autofocus
show
onBarcodeInput={onBarcodeDetected}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { connect } from 'react-redux';

import SelfCheckoutModalComponent from './SelfCheckoutModal';
import { checkoutItem } from '../state/actions';
import { selfCheckOut } from '../state/actions';

const mapDispatchToProps = (dispatch) => ({
checkoutItem: (documentPid, itemPid, patronPid) =>
dispatch(checkoutItem(documentPid, itemPid, patronPid)),
dispatch(selfCheckOut(documentPid, itemPid, patronPid)),
});

const mapStateToProps = (state) => ({
Expand Down
7 changes: 6 additions & 1 deletion src/lib/pages/frontsite/SelfCheckout/index.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import { connect } from 'react-redux';

import SelfCheckoutComponent from './SelfCheckout';
import { selfCheckOutSearch, notifyResultMessage } from './state/actions';
import {
selfCheckOut,
selfCheckOutSearch,
notifyResultMessage,
} from './state/actions';

const mapDispatchToProps = (dispatch) => ({
selfCheckOutSearch: (term) => dispatch(selfCheckOutSearch(term)),
selfCheckOut: (term) => dispatch(selfCheckOut(term)),
notifyResultMessage: (message) => dispatch(notifyResultMessage(message)),
});

Expand Down
38 changes: 12 additions & 26 deletions src/lib/pages/frontsite/SelfCheckout/state/actions.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { itemApi } from '@api/items';
import { loanApi } from '@api/loans';
import {
sendErrorNotification,
sendSuccessNotification,
sendWarningNotification,
} from '@components/Notifications';
import _first from 'lodash/first';
import { FrontSiteRoutes } from '@routes/urls';

export const SEARCH_HAS_ERROR = 'selfCheckOut/SEARCH_HAS_ERROR';
Expand All @@ -20,27 +18,21 @@ export const notifyResultMessage = (message) => {
};
};

const searchItem = async (dispatch, term) => {
const upperCasedTerm = term.toUpperCase();
const response = await itemApi.list(
itemApi.query().withBarcode(upperCasedTerm).qs()
);
const item = _first(response.data.hits) || null;

dispatch({
type: SEARCH_ITEM_SUCCESS,
payload: item,
});
};

export const selfCheckOutSearch = (term) => {
return async (dispatch) => {
dispatch({
type: SEARCH_IS_LOADING,
});

try {
await searchItem(dispatch, term);
const upperCasedTerm = term.toUpperCase();
const response = await loanApi.doSelfCheckoutSearchItem(upperCasedTerm);
const item = response.data || null;

dispatch({
type: SEARCH_ITEM_SUCCESS,
payload: item,
});
} catch (error) {
dispatch({
type: SEARCH_HAS_ERROR,
Expand All @@ -51,20 +43,14 @@ export const selfCheckOutSearch = (term) => {
};
};

export const checkoutItem = (documentPid, itemPid, patronPid) => {
export const selfCheckOut = (documentPid, itemPid, patronPid) => {
return async (dispatch) => {
try {
const response = await loanApi.doCheckout(
documentPid,
itemPid,
patronPid
);
const { pid } = response.data.metadata;
await loanApi.doSelfCheckout(documentPid, itemPid, patronPid);
const linkToLoan = (
<p>
The loan {pid} has been created by you! You can view all your current
loans on your <Link to={FrontSiteRoutes.patronProfile}>profile</Link>{' '}
page.
Self-checkout completed! You can view all your current loans on your{' '}
<Link to={FrontSiteRoutes.patronProfile}>profile</Link> page.
</p>
);
dispatch(sendSuccessNotification('Success!', linkToLoan));
Expand Down
20 changes: 9 additions & 11 deletions src/lib/pages/frontsite/SelfCheckout/state/actions.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,33 @@ import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import * as actions from './actions';
import { initialState } from './reducer';
import { itemApi } from '@api/items';
import { loanApi } from '@api/loans';

const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);

const itemApiListMock = jest.fn();
itemApi.list = itemApiListMock;
const loanApiDoSelfCheckoutSearchItemMock = jest.fn();
loanApi.doSelfCheckoutSearchItem = loanApiDoSelfCheckoutSearchItemMock;

const lowerCasedBarcode = 'cm-145123';
const upperCasedBarcode = lowerCasedBarcode.toUpperCase();

const expectedPayload = { pid: 'item-pid', barcode: upperCasedBarcode };
const response = {
data: {
hits: [expectedPayload],
},
data: expectedPayload,
};

let store;
beforeEach(() => {
itemApiListMock.mockClear();
loanApiDoSelfCheckoutSearchItemMock.mockClear();

store = mockStore(initialState);
store.clearActions();
});

describe('SelfCheck Out test', () => {
it('should dispatch an action updating the payload result item', async () => {
itemApiListMock.mockResolvedValue(response);
loanApiDoSelfCheckoutSearchItemMock.mockResolvedValue(response);

const expectedAction1 = {
type: actions.SEARCH_IS_LOADING,
Expand All @@ -42,14 +40,14 @@ describe('SelfCheck Out test', () => {
await store.dispatch(actions.selfCheckOutSearch(lowerCasedBarcode));
expect(store.getActions()[0]).toEqual(expectedAction1);
expect(store.getActions()[1]).toEqual(expectedAction2);
expect(itemApiListMock).toHaveBeenCalledWith(
itemApi.query().withBarcode(upperCasedBarcode).qs()
expect(loanApiDoSelfCheckoutSearchItemMock).toHaveBeenCalledWith(
upperCasedBarcode
);
});

it('should dispatch an error action when the search fails', async () => {
const errorMsg = 'Error message';
itemApiListMock.mockImplementation(() => {
loanApiDoSelfCheckoutSearchItemMock.mockImplementation(() => {
throw new Error(errorMsg);
});

Expand Down

0 comments on commit 4715615

Please sign in to comment.