1 import {Injectable} from '@angular/core';
2 import {Observable, empty, from} from 'rxjs';
3 import {map, concatMap, mergeMap} from 'rxjs/operators';
4 import {IdlObject} from '@eg/core/idl.service';
5 import {NetService} from '@eg/core/net.service';
6 import {OrgService} from '@eg/core/org.service';
7 import {PcrudService} from '@eg/core/pcrud.service';
8 import {EventService, EgEvent} from '@eg/core/event.service';
9 import {AuthService} from '@eg/core/auth.service';
10 import {BibRecordService, BibRecordSummary} from '@eg/share/catalog/bib-record.service';
11 import {AudioService} from '@eg/share/util/audio.service';
12 import {CircEventsComponent} from './events-dialog.component';
13 import {CircComponentsComponent} from './components.component';
14 import {StringService} from '@eg/share/string/string.service';
15 import {ServerStoreService} from '@eg/core/server-store.service';
16 import {HoldingsService} from '@eg/staff/share/holdings/holdings.service';
17 import {WorkLogService, WorkLogEntry} from '@eg/staff/share/worklog/worklog.service';
19 export interface CircDisplayInfo {
23 copy?: IdlObject; // acp
24 volume?: IdlObject; // acn
25 record?: IdlObject; // bre
26 display?: IdlObject; // mwde
29 const CAN_OVERRIDE_CHECKOUT_EVENTS = [
30 'PATRON_EXCEEDS_OVERDUE_COUNT',
31 'PATRON_EXCEEDS_CHECKOUT_COUNT',
32 'PATRON_EXCEEDS_FINES',
33 'PATRON_EXCEEDS_LONGOVERDUE_COUNT',
35 'CIRC_EXCEEDS_COPY_RANGE',
36 'ITEM_DEPOSIT_REQUIRED',
37 'ITEM_RENTAL_FEE_REQUIRED',
38 'PATRON_EXCEEDS_LOST_COUNT',
39 'COPY_CIRC_NOT_ALLOWED',
43 'ITEM_ON_HOLDS_SHELF',
44 'INVALID_PATRON_ADDRESS',
54 const CHECKOUT_OVERRIDE_AFTER_FIRST = [
55 'PATRON_EXCEEDS_OVERDUE_COUNT',
57 'PATRON_EXCEEDS_LOST_COUNT',
58 'PATRON_EXCEEDS_CHECKOUT_COUNT',
59 'PATRON_EXCEEDS_FINES',
60 'PATRON_EXCEEDS_LONGOVERDUE_COUNT'
63 const CAN_OVERRIDE_RENEW_EVENTS = [
64 'PATRON_EXCEEDS_OVERDUE_COUNT',
65 'PATRON_EXCEEDS_LOST_COUNT',
66 'PATRON_EXCEEDS_CHECKOUT_COUNT',
67 'PATRON_EXCEEDS_FINES',
68 'PATRON_EXCEEDS_LONGOVERDUE_COUNT',
69 'CIRC_EXCEEDS_COPY_RANGE',
70 'ITEM_DEPOSIT_REQUIRED',
71 'ITEM_RENTAL_FEE_REQUIRED',
73 'COPY_CIRC_NOT_ALLOWED',
77 'COPY_NEEDED_FOR_HOLD',
78 'MAX_RENEWALS_REACHED',
79 'CIRC_CLAIMS_RETURNED',
80 'INVALID_PATRON_ADDRESS',
90 // These checkin events do not produce alerts when
91 // options.suppress_alerts is in effect.
92 const CAN_SUPPRESS_CHECKIN_ALERTS = [
96 'PATRON_ACCOUNT_EXPIRED',
98 'CIRC_CLAIMS_RETURNED',
101 'COPY_STATUS_LOST_AND_PAID',
102 'COPY_STATUS_LONG_OVERDUE',
103 'COPY_STATUS_MISSING',
104 'PATRON_EXCEEDS_FINES'
107 const CAN_OVERRIDE_CHECKIN_ALERTS = [
108 // not technically overridable, but special prompt and param
109 'HOLD_CAPTURE_DELAYED',
110 'TRANSIT_CHECKIN_INTERVAL_BLOCK'
111 ].concat(CAN_SUPPRESS_CHECKIN_ALERTS);
114 // API parameter options
115 export interface CheckoutParams {
119 copy_barcode?: string;
121 noncat_type?: number;
122 noncat_count?: number;
125 dummy_title?: string;
126 dummy_author?: string;
128 circ_modifier?: string;
129 void_overdues?: boolean;
130 new_copy_alerts?: boolean;
135 _checkbarcode?: boolean;
136 _worklog?: WorkLogEntry;
139 export interface CircResultCommon {
141 params: CheckinParams | CheckoutParams;
143 allEvents: EgEvent[];
149 parent_circ?: IdlObject;
152 // Set to one of circ_patron or hold_patron depending on the context.
155 // Set to the patron linked to the relevant circulation.
156 circ_patron?: IdlObject;
158 // Set to the patron linked to the relevant hold.
159 hold_patron?: IdlObject;
162 copyAlerts?: IdlObject[];
165 routeTo?: string; // org name or in-branch destination
174 export interface CheckoutResult extends CircResultCommon {
175 params: CheckoutParams;
177 nonCatCirc?: IdlObject;
180 export interface CheckinParams {
183 copy_barcode?: string;
184 claims_never_checked_out?: boolean;
185 void_overdues?: boolean;
186 auto_print_holds_transits?: boolean;
189 next_copy_status?: number[];
190 new_copy_alerts?: boolean;
191 clear_expired?: boolean;
192 hold_as_transit?: boolean;
193 manual_float?: boolean;
194 do_inventory_update?: boolean;
195 no_precat_alert?: boolean;
196 retarget_mode?: string;
198 // internal / local values that are moved from the API request.
200 _worklog?: WorkLogEntry;
201 _checkbarcode?: boolean;
204 export interface CheckinResult extends CircResultCommon {
205 params: CheckinParams;
207 destAddress?: IdlObject;
208 destCourierCode?: string;
212 export class CircService {
213 static resultIndex = 0;
215 components: CircComponentsComponent;
216 nonCatTypes: IdlObject[] = null;
217 autoOverrideCheckoutEvents: {[textcode: string]: boolean} = {};
218 suppressCheckinPopups = false;
219 ignoreCheckinPrecats = false;
220 copyLocationCache: {[id: number]: IdlObject} = {};
221 clearHoldsOnCheckout = false;
222 orgAddrCache: {[addrId: number]: IdlObject} = {};
225 private audio: AudioService,
226 private evt: EventService,
227 private org: OrgService,
228 private net: NetService,
229 private pcrud: PcrudService,
230 private serverStore: ServerStoreService,
231 private strings: StringService,
232 private auth: AuthService,
233 private holdings: HoldingsService,
234 private worklog: WorkLogService,
235 private bib: BibRecordService
238 applySettings(): Promise<any> {
239 return this.serverStore.getItemBatch([
240 'circ.clear_hold_on_checkout',
242 this.clearHoldsOnCheckout = sets['circ.clear_hold_on_checkout'];
243 return this.worklog.loadSettings();
247 // 'circ' is fleshed with copy, vol, bib, wide_display_entry
248 // Extracts some display info from a fleshed circ.
249 getDisplayInfo(circ: IdlObject): CircDisplayInfo {
250 return this.getCopyDisplayInfo(circ.target_copy());
253 getCopyDisplayInfo(copy: IdlObject): CircDisplayInfo {
255 if (copy.call_number() === -1 || copy.call_number().id() === -1) {
258 title: copy.dummy_title(),
259 author: copy.dummy_author(),
260 isbn: copy.dummy_isbn(),
265 const volume = copy.call_number();
266 const record = volume.record();
267 const display = record.wide_display_entry();
269 let isbn = JSON.parse(display.isbn());
270 if (Array.isArray(isbn)) { isbn = isbn.join(','); }
273 title: JSON.parse(display.title()),
274 author: JSON.parse(display.author()),
283 getOrgAddr(orgId: number, addrType): Promise<IdlObject> {
284 const org = this.org.get(orgId);
285 const addrId = this.org[addrType];
287 if (!addrId) { return Promise.resolve(null); }
289 if (this.orgAddrCache[addrId]) {
290 return Promise.resolve(this.orgAddrCache[addrId]);
293 return this.pcrud.retrieve('aoa', addrId).toPromise()
295 this.orgAddrCache[addrId] = addr;
300 // find the open transit for the given copy barcode; flesh the org
302 // Sets result.transit
303 findCopyTransit(result: CircResultCommon): Promise<IdlObject> {
304 // NOTE: result.transit may exist, but it's not necessarily
305 // the transit we want, since a transit close + open in the API
306 // returns the closed transit.
307 return this.findCopyTransitById(result.copy.id())
309 result.transit = transit;
314 findCopyTransitById(copyId: number): Promise<IdlObject> {
315 return this.pcrud.search('atc', {
316 dest_recv_time : null,
321 order_by : {atc : 'source_send_time desc'},
322 }, {authoritative : true}
323 ).toPromise().then(transit => {
325 transit.source(this.org.get(transit.source()));
326 transit.dest(this.org.get(transit.dest()));
330 return Promise.reject('No transit found');
334 // Sets result.transit and result.copy
335 findCopyTransitByBarcode(result: CircResultCommon): Promise<IdlObject> {
336 // NOTE: result.transit may exist, but it's not necessarily
337 // the transit we want, since a transit close + open in the API
338 // returns the closed transit.
340 const barcode = result.params.copy_barcode;
342 return this.pcrud.search('atc', {
343 dest_recv_time : null,
347 flesh_fields : {atc : ['target_copy']},
357 order_by : {atc : 'source_send_time desc'}
358 }, {authoritative : true}
360 ).toPromise().then(transit => {
362 transit.source(this.org.get(transit.source()));
363 transit.dest(this.org.get(transit.dest()));
364 result.transit = transit;
365 result.copy = transit.target_copy();
368 return Promise.reject('No transit found');
372 getNonCatTypes(): Promise<IdlObject[]> {
374 if (this.nonCatTypes) {
375 return Promise.resolve(this.nonCatTypes);
378 return this.pcrud.search('cnct',
379 {owning_lib: this.org.fullPath(this.auth.user().ws_ou(), true)},
380 {order_by: {cnct: 'name'}},
382 ).toPromise().then(types => this.nonCatTypes = types);
385 // Remove internal tracking variables on Param objects so they are
386 // not sent to the server, which can result in autoload errors.
388 params: CheckoutParams | CheckinParams): CheckoutParams | CheckinParams {
390 const apiParams = Object.assign({}, params); // clone
391 const remove = Object.keys(apiParams).filter(k => k.match(/^_/));
392 remove.forEach(p => delete apiParams[p]);
394 // This modifier is not sent to the server.
395 // Should be _-prefixed, but we already have a workstation setting,
396 // etc. for this one. Just manually remove it from the API params.
397 delete apiParams['auto_print_holds_transits'];
402 checkout(params: CheckoutParams): Promise<CheckoutResult> {
404 params.new_copy_alerts = true;
405 params._renewal = false;
406 console.debug('checking out with', params);
408 let method = 'open-ils.circ.checkout.full';
409 if (params._override) { method += '.override'; }
411 return this.inspectBarcode(params).then(barcodeOk => {
412 if (!barcodeOk) { return null; }
414 return this.net.request(
415 'open-ils.circ', method,
416 this.auth.token(), this.apiParams(params)).toPromise()
417 .then(result => this.unpackCheckoutData(params, result))
418 .then(result => this.processCheckoutResult(result));
422 renew(params: CheckoutParams): Promise<CheckoutResult> {
424 params.new_copy_alerts = true;
425 params._renewal = true;
426 console.debug('renewing out with', params);
428 let method = 'open-ils.circ.renew';
429 if (params._override) { method += '.override'; }
431 return this.inspectBarcode(params).then(barcodeOk => {
432 if (!barcodeOk) { return null; }
434 return this.net.request(
435 'open-ils.circ', method,
436 this.auth.token(), this.apiParams(params)).toPromise()
437 .then(result => this.unpackCheckoutData(params, result))
438 .then(result => this.processCheckoutResult(result));
444 params: CheckoutParams, response: any): Promise<CheckoutResult> {
446 const allEvents = Array.isArray(response) ?
447 response.map(r => this.evt.parse(r)) :
448 [this.evt.parse(response)];
450 console.debug('checkout events', allEvents.map(e => e.textcode));
451 console.debug('checkout returned', allEvents);
453 const firstEvent = allEvents[0];
454 const payload = firstEvent.payload;
456 const result: CheckoutResult = {
457 index: CircService.resultIndex++,
458 firstEvent: firstEvent,
459 allEvents: allEvents,
464 // Some scenarios (e.g. copy in transit) have no payload,
466 if (!payload) { return Promise.resolve(result); }
468 result.circ = payload.circ;
469 result.copy = payload.copy;
470 result.volume = payload.volume;
471 result.patron = payload.patron;
472 result.record = payload.record;
473 result.nonCatCirc = payload.noncat_circ;
475 return this.fleshCommonData(result).then(_ => {
476 const action = params._renewal ? 'renew' :
477 (params.noncat ? 'noncat_checkout' : 'checkout');
478 this.addWorkLog(action, result);
483 processCheckoutResult(result: CheckoutResult): Promise<CheckoutResult> {
484 const renewing = result.params._renewal;
485 const key = renewing ? 'renew' : 'checkout';
487 const overridable = renewing ?
488 CAN_OVERRIDE_RENEW_EVENTS : CAN_OVERRIDE_CHECKOUT_EVENTS;
490 if (result.allEvents.filter(
491 e => overridable.includes(e.textcode)).length > 0) {
492 return this.handleOverridableCheckoutEvents(result);
495 switch (result.firstEvent.textcode) {
497 result.success = true;
498 this.audio.play(`success.${key}`);
499 return Promise.resolve(result);
501 case 'ITEM_NOT_CATALOGED':
502 return this.handlePrecat(result);
504 case 'OPEN_CIRCULATION_EXISTS':
506 if (result.firstEvent.payload.auto_renew) {
507 const coParams = Object.assign({}, result.params); // clone
508 return this.renew(coParams);
511 return this.handleOpenCirc(result);
513 case 'COPY_IN_TRANSIT':
514 this.audio.play(`warning.${key}.in_transit`);
515 return this.copyInTransitDialog(result);
517 case 'PATRON_CARD_INACTIVE':
518 case 'PATRON_INACTIVE':
519 case 'PATRON_ACCOUNT_EXPIRED':
520 case 'CIRC_CLAIMS_RETURNED':
521 case 'ACTOR_USER_NOT_FOUND':
522 case 'AVAIL_HOLD_COPY_RATIO_EXCEEDED':
523 this.audio.play(`warning.${key}`);
524 return this.exitAlert({
525 textcode: result.firstEvent.textcode,
526 barcode: result.params.copy_barcode
529 case 'ASSET_COPY_NOT_FOUND':
530 this.audio.play(`error.${key}.not_found`);
531 return this.exitAlert({
532 textcode: result.firstEvent.textcode,
533 barcode: result.params.copy_barcode
537 this.audio.play(`error.${key}.unknown`);
538 return this.exitAlert({
539 textcode: 'CHECKOUT_FAILED_GENERIC',
540 barcode: result.params.copy_barcode
545 exitAlert(context: any): Promise<any> {
546 const key = 'staff.circ.events.' + context.textcode;
547 return this.strings.interpolate(key, context)
549 this.components.circFailedDialog.dialogBody = str;
550 return this.components.circFailedDialog.open().toPromise();
552 .then(_ => Promise.reject('Bailling on event ' + context.textcode));
555 copyInTransitDialog(result: CheckoutResult): Promise<CheckoutResult> {
556 this.components.copyInTransitDialog.checkout = result;
558 return this.findCopyTransitByBarcode(result)
559 .then(_ => this.components.copyInTransitDialog.open().toPromise())
560 .then(cancelAndCheckout => {
561 if (cancelAndCheckout) {
563 return this.abortTransit(result.transit.id())
565 // We had to look up the copy from the barcode since
566 // it was not embedded in the result event. Since
567 // we have the specifics on the copy, go ahead and
568 // copy them into the params we use for the follow
570 result.params.copy_barcode = result.copy.barcode();
571 result.params.copy_id = result.copy.id();
572 return this.checkout(result.params);
581 // Ask the user if we should resolve the circulation and check
582 // out to the user or leave it alone.
583 // When resolving and checking out, renew if it's for the same
584 // user, otherwise check it in, then back out to the current user.
585 handleOpenCirc(result: CheckoutResult): Promise<CheckoutResult> {
587 let sameUser = false;
589 return this.net.request(
591 'open-ils.circ.copy_checkout_history.retrieve',
592 this.auth.token(), result.params.copy_id, 1).toPromise()
595 const circ = circs[0];
597 sameUser = result.params.patron_id === circ.usr();
598 this.components.openCircDialog.sameUser = sameUser;
599 this.components.openCircDialog.circDate = circ.xact_start();
601 return this.components.openCircDialog.open({size: 'lg'}).toPromise();
604 .then(fromDialog => {
606 // Leave the open circ checked out.
607 if (!fromDialog) { return result; }
609 const coParams = Object.assign({}, result.params); // clone
611 if (fromDialog.renew) {
612 coParams.void_overdues = fromDialog.forgiveFines;
613 return this.renew(coParams);
616 const ciParams: CheckinParams = {
618 copy_id: coParams.copy_id,
619 void_overdues: fromDialog.forgiveFines
622 return this.checkin(ciParams)
625 return this.checkout(coParams);
627 return Promise.reject('Unable to check in item');
633 handleOverridableCheckoutEvents(result: CheckoutResult): Promise<CheckoutResult> {
634 const params = result.params;
635 const firstEvent = result.firstEvent;
636 const events = result.allEvents;
638 if (params._override) {
639 // Should never get here. Just being safe.
640 return Promise.reject(null);
644 e => !this.autoOverrideCheckoutEvents[e.textcode]).length === 0) {
645 // User has already seen all of these events and overridden them,
646 // so avoid showing them again since they are all auto-overridable.
647 params._override = true;
648 return params._renewal ? this.renew(params) : this.checkout(params);
651 // New-style alerts are reported via COPY_ALERT_MESSAGE and
652 // includes the alerts in the payload as an array.
653 if (firstEvent.textcode === 'COPY_ALERT_MESSAGE'
654 && Array.isArray(firstEvent.payload)) {
655 this.components.copyAlertManager.alerts = firstEvent.payload;
657 this.components.copyAlertManager.mode =
658 params._renewal ? 'renew' : 'checkout';
660 return this.components.copyAlertManager.open().toPromise()
663 params._override = true;
664 return this.checkout(params);
669 return this.showOverrideDialog(result, events);
672 showOverrideDialog(result: CheckoutResult,
673 events: EgEvent[], checkin?: boolean): Promise<CheckoutResult> {
675 const params = result.params;
676 const mode = checkin ? 'checkin' : (params._renewal ? 'renew' : 'checkout');
678 const holdShelfEvent = events.filter(e => e.textcode === 'ITEM_ON_HOLDS_SHELF')[0];
680 if (holdShelfEvent) {
681 this.components.circEventsDialog.clearHolds = this.clearHoldsOnCheckout;
682 this.components.circEventsDialog.patronId = holdShelfEvent.payload.patron_id;
683 this.components.circEventsDialog.patronName = holdShelfEvent.payload.patron_name;
686 this.components.circEventsDialog.copyBarcode = result.params.copy_barcode;
687 this.components.circEventsDialog.events = events;
688 this.components.circEventsDialog.mode = mode;
690 return this.components.circEventsDialog.open().toPromise()
692 const confirmed = resp.override;
693 if (!confirmed) { return null; }
695 let promise = Promise.resolve(null);
698 // Indicate these events have been seen and overridden.
699 events.forEach(evt => {
700 if (CHECKOUT_OVERRIDE_AFTER_FIRST.includes(evt.textcode)) {
701 this.autoOverrideCheckoutEvents[evt.textcode] = true;
705 if (holdShelfEvent && resp.clearHold) {
706 const holdId = holdShelfEvent.payload.hold_id;
708 // Cancel the hold that put our checkout item
709 // on the holds shelf.
711 promise = promise.then(_ => {
712 return this.net.request(
714 'open-ils.circ.hold.cancel',
718 'Item checked out by other patron' // FIXME I18n
724 return promise.then(_ => {
725 params._override = true;
726 return this[mode](params); // checkout/renew/checkin
731 handlePrecat(result: CheckoutResult): Promise<CheckoutResult> {
732 this.components.precatDialog.barcode = result.params.copy_barcode;
734 return this.components.precatDialog.open().toPromise().then(values => {
736 if (values && values.dummy_title) {
737 const params = result.params;
738 params.precat = true;
739 Object.keys(values).forEach(key => params[key] = values[key]);
740 return this.checkout(params);
743 result.canceled = true;
744 return Promise.resolve(result);
748 checkin(params: CheckinParams): Promise<CheckinResult> {
749 params.new_copy_alerts = true;
751 console.debug('checking in with', params);
753 let method = 'open-ils.circ.checkin';
754 if (params._override) { method += '.override'; }
756 return this.inspectBarcode(params).then(barcodeOk => {
757 if (!barcodeOk) { return null; }
759 return this.net.request(
760 'open-ils.circ', method,
761 this.auth.token(), this.apiParams(params)).toPromise()
762 .then(result => this.unpackCheckinData(params, result))
763 .then(result => this.processCheckinResult(result));
767 fetchPatron(userId: number): Promise<IdlObject> {
768 return this.pcrud.retrieve('au', userId, {
770 flesh_fields : {'au' : ['card', 'stat_cat_entries']}
775 fleshCommonData(result: CircResultCommon): Promise<CircResultCommon> {
777 console.warn('fleshCommonData()');
779 const copy = result.copy;
780 const volume = result.volume;
781 const circ = result.circ;
782 const hold = result.hold;
783 const nonCatCirc = (result as CheckoutResult).nonCatCirc;
785 let promise: Promise<any> = Promise.resolve();
788 console.debug('fleshCommonData() hold ', hold.usr());
789 promise = promise.then(_ => {
790 return this.fetchPatron(hold.usr())
792 result.hold_patron = usr;
793 console.debug('Setting hold patron to ' + usr.id());
798 const circPatronId = circ ? circ.usr() :
799 (nonCatCirc ? nonCatCirc.patron() : null);
802 console.debug('fleshCommonData() circ patron id', circPatronId);
803 promise = promise.then(_ => {
804 return this.fetchPatron(circPatronId)
806 result.circ_patron = usr;
807 console.debug('Setting circ patron to ' + usr.id());
812 // Set a default patron value which is used in most cases.
813 promise = promise.then(_ => {
814 result.patron = result.hold_patron || result.circ_patron;
818 result.title = result.record.title();
819 result.author = result.record.author();
820 result.isbn = result.record.isbn();
823 result.title = result.copy.dummy_title();
824 result.author = result.copy.dummy_author();
825 result.isbn = result.copy.dummy_isbn();
829 if (this.copyLocationCache[copy.location()]) {
830 copy.location(this.copyLocationCache[copy.location()]);
832 promise = promise.then(_ => {
833 return this.pcrud.retrieve('acpl', copy.location())
834 .toPromise().then(loc => {
836 this.copyLocationCache[loc.id()] = loc;
841 if (typeof copy.status() !== 'object') {
842 promise = promise.then(_ => this.holdings.getCopyStatuses())
845 Object.values(stats).filter(s => s.id() === copy.status())[0];
846 if (stat) { copy.status(stat); }
851 promise = promise.then(_ => {
852 // By default, all items route-to their location.
853 // Value replaced later on as needed.
854 if (copy && typeof copy.location() === 'object') {
855 result.routeTo = copy.location().name();
860 // Flesh volume prefixes and suffixes
862 if (typeof volume.prefix() !== 'object') {
863 promise = promise.then(_ =>
864 this.pcrud.retrieve('acnp', volume.prefix()).toPromise()
865 ).then(p => volume.prefix(p));
868 if (typeof volume.suffix() !== 'object') {
869 promise = promise.then(_ =>
870 this.pcrud.retrieve('acns', volume.suffix()).toPromise()
871 ).then(p => volume.suffix(p));
875 return promise.then(_ => result);
878 unpackCheckinData(params: CheckinParams, response: any): Promise<CheckinResult> {
879 const allEvents = Array.isArray(response) ?
880 response.map(r => this.evt.parse(r)) : [this.evt.parse(response)];
882 console.debug('checkin events', allEvents.map(e => e.textcode));
883 console.debug('checkin response', response);
885 const firstEvent = allEvents[0];
886 const payload = firstEvent.payload;
889 firstEvent.textcode.match(/SUCCESS|NO_CHANGE|ROUTE_ITEM/) !== null;
891 const result: CheckinResult = {
892 index: CircService.resultIndex++,
893 firstEvent: firstEvent,
894 allEvents: allEvents,
900 // e.g. ASSET_COPY_NOT_FOUND
901 return Promise.resolve(result);
904 result.circ = payload.circ;
905 result.parent_circ = payload.parent_circ;
906 result.copy = payload.copy;
907 result.volume = payload.volume;
908 result.record = payload.record;
909 result.transit = payload.transit;
910 result.hold = payload.hold;
912 const copy = result.copy;
913 const volume = result.volume;
914 const transit = result.transit;
915 const circ = result.circ;
916 const parent_circ = result.parent_circ;
919 if (typeof transit.dest() !== 'object') {
920 transit.dest(this.org.get(transit.dest()));
922 if (typeof transit.source() !== 'object') {
923 transit.source(this.org.get(transit.source()));
927 // for checkin, the mbts lives on the main circ
928 if (circ && circ.billable_transaction()) {
929 result.mbts = circ.billable_transaction().summary();
932 // on renewals, the mbts lives on the parent circ
933 if (parent_circ && parent_circ.billable_transaction()) {
934 result.mbts = parent_circ.billable_transaction().summary();
937 return this.fleshCommonData(result).then(_ => {
938 this.addWorkLog('checkin', result);
943 processCheckinResult(result: CheckinResult): Promise<CheckinResult> {
944 const params = result.params;
945 const allEvents = result.allEvents;
947 // Informational alerts that can be ignored if configured.
948 if (this.suppressCheckinPopups &&
949 allEvents.filter(e =>
950 !CAN_SUPPRESS_CHECKIN_ALERTS.includes(e.textcode)).length === 0) {
952 // Should not be necessary, but good to be safe.
953 if (params._override) { return Promise.resolve(null); }
955 params._override = true;
956 return this.checkin(params);
959 // Alerts that require a manual override.
960 if (allEvents.filter(
961 e => CAN_OVERRIDE_CHECKIN_ALERTS.includes(e.textcode)).length > 0) {
962 return this.handleOverridableCheckinEvents(result);
965 switch (result.firstEvent.textcode) {
968 return this.handleCheckinSuccess(result);
970 case 'ITEM_NOT_CATALOGED':
971 this.audio.play('error.checkout.no_cataloged');
972 result.routeTo = this.components.catalogingStr.text;
973 return this.showPrecatAlert().then(_ => result);
976 this.audio.play(result.hold ?
977 'info.checkin.transit.hold' : 'info.checkin.transit');
980 console.debug('Skipping route dialog on "noop" checkin');
981 return Promise.resolve(result);
984 this.components.routeDialog.checkin = result;
985 return this.findCopyTransit(result)
986 .then(_ => this.components.routeDialog.open().toPromise())
989 case 'ASSET_COPY_NOT_FOUND':
990 this.audio.play('error.checkin.not_found');
991 return this.handleCheckinUncatAlert(result);
994 this.audio.play('error.checkin.unknown');
996 'Unhandled checkin response : ' + result.firstEvent.textcode);
999 return Promise.resolve(result);
1002 addWorkLog(action: string, result: CircResultCommon) {
1003 const params = result.params;
1004 const copy = result.copy;
1005 const patron = result.patron;
1007 // Some worklog data may be provided by the caller in the params.
1008 const entry: WorkLogEntry =
1009 Object.assign(params._worklog || {}, {action: action});
1012 entry.item = copy.barcode();
1013 entry.item_id = copy.id();
1015 entry.item = params.copy_barcode;
1016 entry.item_id = params.copy_id;
1020 entry.patron_id = patron.id();
1021 entry.user = patron.family_name();
1025 entry.hold_id = result.hold.id();
1028 this.worklog.record(entry);
1031 showPrecatAlert(): Promise<any> {
1032 if (!this.suppressCheckinPopups && !this.ignoreCheckinPrecats) {
1033 // Tell the user its a precat and return the result.
1034 return this.components.routeToCatalogingDialog.open()
1037 return Promise.resolve(null);
1040 handleCheckinSuccess(result: CheckinResult): Promise<CheckinResult> {
1041 const copy = result.copy;
1043 if (!copy) { return Promise.resolve(result); }
1045 const stat = copy.status();
1046 const statId = typeof stat === 'object' ? stat.id() : stat;
1050 case 0: /* AVAILABLE */
1051 case 4: /* MISSING */
1052 case 7: /* RESHELVING */
1053 this.audio.play('success.checkin');
1054 return this.handleCheckinLocAlert(result);
1056 case 8: /* ON HOLDS SHELF */
1057 this.audio.play('info.checkin.holds_shelf');
1059 const hold = result.hold;
1063 if (Number(hold.pickup_lib()) === Number(this.auth.user().ws_ou())) {
1064 result.routeTo = this.components.holdShelfStr.text;
1065 this.components.routeDialog.checkin = result;
1066 return this.components.routeDialog.open().toPromise()
1070 // Should not happen in practice, but to be safe.
1071 this.audio.play('warning.checkin.wrong_shelf');
1075 console.warn('API Returned insufficient info on holds');
1079 case 11: /* CATALOGING */
1080 this.audio.play('info.checkin.cataloging');
1081 result.routeTo = this.components.catalogingStr.text;
1082 return this.showPrecatAlert().then(_ => result);
1084 case 15: /* ON_RESERVATION_SHELF */
1085 this.audio.play('info.checkin.reservation');
1089 this.audio.play('success.checkin');
1090 console.debug(`Unusual checkin copy status (may have been
1091 set via copy alert): status=${statId}`);
1094 return Promise.resolve(result);
1097 handleCheckinLocAlert(result: CheckinResult): Promise<CheckinResult> {
1098 const copy = result.copy;
1100 if (this.suppressCheckinPopups
1101 || copy.location().checkin_alert() === 'f') {
1102 return Promise.resolve(result);
1105 return this.strings.interpolate(
1106 'staff.circ.checkin.location.alert',
1107 {barcode: copy.barcode(), location: copy.location().name()}
1109 this.components.locationAlertDialog.dialogBody = str;
1110 return this.components.locationAlertDialog.open().toPromise()
1115 handleCheckinUncatAlert(result: CheckinResult): Promise<CheckinResult> {
1116 const barcode = result.copy ?
1117 result.copy.barcode() : result.params.copy_barcode;
1119 if (this.suppressCheckinPopups) {
1120 return Promise.resolve(result);
1123 return this.strings.interpolate(
1124 'staff.circ.checkin.uncat.alert', {barcode: barcode}
1126 this.components.uncatAlertDialog.dialogBody = str;
1127 return this.components.uncatAlertDialog.open().toPromise()
1133 handleOverridableCheckinEvents(result: CheckinResult): Promise<CheckinResult> {
1134 const params = result.params;
1135 const events = result.allEvents;
1136 const firstEvent = result.firstEvent;
1138 if (params._override) {
1139 // Should never get here. Just being safe.
1140 return Promise.reject(null);
1143 if (this.suppressCheckinPopups && events.filter(
1144 e => !CAN_SUPPRESS_CHECKIN_ALERTS.includes(e.textcode)).length === 0) {
1145 // These events are automatically overridden when suppress
1146 // popups are in effect.
1147 params._override = true;
1148 return this.checkin(params);
1151 // New-style alerts are reported via COPY_ALERT_MESSAGE and
1152 // includes the alerts in the payload as an array.
1153 if (firstEvent.textcode === 'COPY_ALERT_MESSAGE'
1154 && Array.isArray(firstEvent.payload)) {
1155 this.components.copyAlertManager.alerts = firstEvent.payload;
1156 this.components.copyAlertManager.mode = 'checkin';
1158 return this.components.copyAlertManager.open().toPromise()
1161 if (!resp) { return result; } // dialog was canceled
1163 if (resp.nextStatus !== null) {
1164 params.next_copy_status = [resp.nextStatus];
1165 params.capture = 'nocapture';
1168 params._override = true;
1170 return this.checkin(params);
1174 return this.showOverrideDialog(result, events, true);
1178 // The provided params (minus the copy_id) will be used
1180 checkoutBatch(copyIds: number[],
1181 params: CheckoutParams): Observable<CheckoutResult> {
1183 if (copyIds.length === 0) { return empty(); }
1185 return from(copyIds).pipe(concatMap(id => {
1186 const cparams = Object.assign({}, params); // clone
1187 cparams.copy_id = id;
1188 return from(this.checkout(cparams));
1192 // The provided params (minus the copy_id) will be used
1194 renewBatch(copyIds: number[],
1195 params?: CheckoutParams): Observable<CheckoutResult> {
1197 if (copyIds.length === 0) { return empty(); }
1198 if (!params) { params = {}; }
1200 return from(copyIds).pipe(concatMap(id => {
1201 const cparams = Object.assign({}, params); // clone
1202 cparams.copy_id = id;
1203 return from(this.renew(cparams));
1207 // The provided params (minus the copy_id) will be used
1209 checkinBatch(copyIds: number[],
1210 params?: CheckinParams): Observable<CheckinResult> {
1212 if (copyIds.length === 0) { return empty(); }
1213 if (!params) { params = {}; }
1215 return from(copyIds).pipe(concatMap(id => {
1216 const cparams = Object.assign({}, params); // clone
1217 cparams.copy_id = id;
1218 return from(this.checkin(cparams));
1222 abortTransit(transitId: number): Promise<any> {
1223 return this.net.request(
1225 'open-ils.circ.transit.abort',
1226 this.auth.token(), {transitid : transitId}
1227 ).toPromise().then(resp => {
1228 const evt = this.evt.parse(resp);
1231 return Promise.reject(evt.toString());
1233 return Promise.resolve();
1237 lastCopyCirc(copyId: number): Promise<IdlObject> {
1238 return this.pcrud.search('circ',
1239 {target_copy : copyId},
1240 {order_by : {circ : 'xact_start desc' }, limit : 1}
1244 // Resolves to true if the barcode is OK or the user confirmed it or
1245 // the user doesn't care to begin with
1246 inspectBarcode(params: CheckoutParams | CheckinParams): Promise<boolean> {
1247 if (!params._checkbarcode || !params.copy_barcode) {
1248 return Promise.resolve(true);
1251 if (this.checkBarcode(params.copy_barcode)) {
1252 // Avoid prompting again on an override
1253 params._checkbarcode = false;
1254 return Promise.resolve(true);
1257 this.components.badBarcodeDialog.barcode = params.copy_barcode;
1258 return this.components.badBarcodeDialog.open().toPromise()
1259 // Avoid prompting again on an override
1261 params._checkbarcode = false
1266 checkBarcode(barcode: string): boolean {
1267 if (barcode !== Number(barcode).toString()) { return false; }
1269 const bc = barcode.toString();
1271 // "16.00" == Number("16.00"), but the . is bad.
1272 // Throw out any barcode that isn't just digits
1273 if (bc.search(/\D/) !== -1) { return false; }
1275 const lastDigit = bc.substr(bc.length - 1);
1276 const strippedBarcode = bc.substr(0, bc.length - 1);
1277 return this.barcodeCheckdigit(strippedBarcode).toString() === lastDigit;
1280 barcodeCheckdigit(bc: string): number {
1283 const reverseBarcode = bc.toString().split('').reverse();
1285 reverseBarcode.forEach(ch => {
1287 const product = (Number(ch) * multiplier) + '';
1288 product.split('').forEach(num => tempSum += Number(num));
1289 checkSum += Number(tempSum);
1290 multiplier = multiplier === 2 ? 1 : 2;
1293 const cSumStr = checkSum.toString();
1294 const nextMultipleOf10 =
1295 (Number(cSumStr.match(/(\d*)\d$/)[1]) * 10) + 10;
1297 let checkDigit = nextMultipleOf10 - Number(cSumStr);
1298 if (checkDigit === 10) { checkDigit = 0; }