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',
53 const CHECKOUT_OVERRIDE_AFTER_FIRST = [
54 'PATRON_EXCEEDS_OVERDUE_COUNT',
56 'PATRON_EXCEEDS_LOST_COUNT',
57 'PATRON_EXCEEDS_CHECKOUT_COUNT',
58 'PATRON_EXCEEDS_FINES',
59 'PATRON_EXCEEDS_LONGOVERDUE_COUNT'
62 const CAN_OVERRIDE_RENEW_EVENTS = [
63 'PATRON_EXCEEDS_OVERDUE_COUNT',
64 'PATRON_EXCEEDS_LOST_COUNT',
65 'PATRON_EXCEEDS_CHECKOUT_COUNT',
66 'PATRON_EXCEEDS_FINES',
67 'PATRON_EXCEEDS_LONGOVERDUE_COUNT',
68 'CIRC_EXCEEDS_COPY_RANGE',
69 'ITEM_DEPOSIT_REQUIRED',
70 'ITEM_RENTAL_FEE_REQUIRED',
72 'COPY_CIRC_NOT_ALLOWED',
76 'COPY_NEEDED_FOR_HOLD',
77 'MAX_RENEWALS_REACHED',
78 'CIRC_CLAIMS_RETURNED',
88 // These checkin events do not produce alerts when
89 // options.suppress_alerts is in effect.
90 const CAN_SUPPRESS_CHECKIN_ALERTS = [
94 'PATRON_ACCOUNT_EXPIRED',
96 'CIRC_CLAIMS_RETURNED',
99 'COPY_STATUS_LOST_AND_PAID',
100 'COPY_STATUS_LONG_OVERDUE',
101 'COPY_STATUS_MISSING',
102 'PATRON_EXCEEDS_FINES'
105 const CAN_OVERRIDE_CHECKIN_ALERTS = [
106 // not technically overridable, but special prompt and param
107 'HOLD_CAPTURE_DELAYED',
108 'TRANSIT_CHECKIN_INTERVAL_BLOCK'
109 ].concat(CAN_SUPPRESS_CHECKIN_ALERTS);
112 // API parameter options
113 export interface CheckoutParams {
117 copy_barcode?: string;
119 noncat_type?: number;
120 noncat_count?: number;
123 dummy_title?: string;
124 dummy_author?: string;
126 circ_modifier?: string;
127 void_overdues?: boolean;
128 new_copy_alerts?: boolean;
133 _checkbarcode?: boolean;
134 _worklog?: WorkLogEntry;
137 export interface CircResultCommon {
139 params: CheckinParams | CheckoutParams;
141 allEvents: EgEvent[];
147 parent_circ?: IdlObject;
151 copyAlerts?: IdlObject[];
161 export interface CheckoutResult extends CircResultCommon {
162 params: CheckoutParams;
164 nonCatCirc?: IdlObject;
167 export interface CheckinParams {
170 copy_barcode?: string;
171 claims_never_checked_out?: boolean;
172 void_overdues?: boolean;
173 auto_print_holds_transits?: boolean;
176 next_copy_status?: number[];
177 new_copy_alerts?: boolean;
178 clear_expired?: boolean;
179 hold_as_transit?: boolean;
180 manual_float?: boolean;
181 do_inventory_update?: boolean;
182 no_precat_alert?: boolean;
183 retarget_mode?: string;
185 // internal / local values that are moved from the API request.
187 _worklog?: WorkLogEntry;
188 _checkbarcode?: boolean;
191 export interface CheckinResult extends CircResultCommon {
192 params: CheckinParams;
193 routeTo?: string; // org name or in-branch destination
195 destAddress?: IdlObject;
196 destCourierCode?: string;
200 export class CircService {
201 static resultIndex = 0;
203 components: CircComponentsComponent;
204 nonCatTypes: IdlObject[] = null;
205 autoOverrideCheckoutEvents: {[textcode: string]: boolean} = {};
206 suppressCheckinPopups = false;
207 ignoreCheckinPrecats = false;
208 copyLocationCache: {[id: number]: IdlObject} = {};
209 clearHoldsOnCheckout = false;
210 orgAddrCache: {[addrId: number]: IdlObject} = {};
213 private audio: AudioService,
214 private evt: EventService,
215 private org: OrgService,
216 private net: NetService,
217 private pcrud: PcrudService,
218 private serverStore: ServerStoreService,
219 private strings: StringService,
220 private auth: AuthService,
221 private holdings: HoldingsService,
222 private worklog: WorkLogService,
223 private bib: BibRecordService
226 applySettings(): Promise<any> {
227 return this.serverStore.getItemBatch([
228 'circ.clear_hold_on_checkout',
230 this.clearHoldsOnCheckout = sets['circ.clear_hold_on_checkout'];
231 return this.worklog.loadSettings();
235 // 'circ' is fleshed with copy, vol, bib, wide_display_entry
236 // Extracts some display info from a fleshed circ.
237 getDisplayInfo(circ: IdlObject): CircDisplayInfo {
238 return this.getCopyDisplayInfo(circ.target_copy());
241 getCopyDisplayInfo(copy: IdlObject): CircDisplayInfo {
243 if (copy.call_number() === -1 || copy.call_number().id() === -1) {
246 title: copy.dummy_title(),
247 author: copy.dummy_author(),
248 isbn: copy.dummy_isbn(),
253 const volume = copy.call_number();
254 const record = volume.record();
255 const display = record.wide_display_entry();
257 let isbn = JSON.parse(display.isbn());
258 if (Array.isArray(isbn)) { isbn = isbn.join(','); }
261 title: JSON.parse(display.title()),
262 author: JSON.parse(display.author()),
271 getOrgAddr(orgId: number, addrType): Promise<IdlObject> {
272 const org = this.org.get(orgId);
273 const addrId = this.org[addrType];
275 if (!addrId) { return Promise.resolve(null); }
277 if (this.orgAddrCache[addrId]) {
278 return Promise.resolve(this.orgAddrCache[addrId]);
281 return this.pcrud.retrieve('aoa', addrId).toPromise()
283 this.orgAddrCache[addrId] = addr;
288 // find the open transit for the given copy barcode; flesh the org
290 // Sets result.transit
291 findCopyTransit(result: CircResultCommon): Promise<IdlObject> {
292 // NOTE: result.transit may exist, but it's not necessarily
293 // the transit we want, since a transit close + open in the API
294 // returns the closed transit.
295 return this.findCopyTransitById(result.copy.id())
297 result.transit = transit;
302 findCopyTransitById(copyId: number): Promise<IdlObject> {
303 return this.pcrud.search('atc', {
304 dest_recv_time : null,
309 order_by : {atc : 'source_send_time desc'},
310 }, {authoritative : true}
311 ).toPromise().then(transit => {
313 transit.source(this.org.get(transit.source()));
314 transit.dest(this.org.get(transit.dest()));
318 return Promise.reject('No transit found');
322 // Sets result.transit and result.copy
323 findCopyTransitByBarcode(result: CircResultCommon): Promise<IdlObject> {
324 // NOTE: result.transit may exist, but it's not necessarily
325 // the transit we want, since a transit close + open in the API
326 // returns the closed transit.
328 const barcode = result.params.copy_barcode;
330 return this.pcrud.search('atc', {
331 dest_recv_time : null,
335 flesh_fields : {atc : ['target_copy']},
345 order_by : {atc : 'source_send_time desc'}
346 }, {authoritative : true}
348 ).toPromise().then(transit => {
350 transit.source(this.org.get(transit.source()));
351 transit.dest(this.org.get(transit.dest()));
352 result.transit = transit;
353 result.copy = transit.target_copy();
356 return Promise.reject('No transit found');
360 getNonCatTypes(): Promise<IdlObject[]> {
362 if (this.nonCatTypes) {
363 return Promise.resolve(this.nonCatTypes);
366 return this.pcrud.search('cnct',
367 {owning_lib: this.org.fullPath(this.auth.user().ws_ou(), true)},
368 {order_by: {cnct: 'name'}},
370 ).toPromise().then(types => this.nonCatTypes = types);
373 // Remove internal tracking variables on Param objects so they are
374 // not sent to the server, which can result in autoload errors.
376 params: CheckoutParams | CheckinParams): CheckoutParams | CheckinParams {
378 const apiParams = Object.assign({}, params); // clone
379 const remove = Object.keys(apiParams).filter(k => k.match(/^_/));
380 remove.forEach(p => delete apiParams[p]);
382 // This modifier is not sent to the server.
383 // Should be _-prefixed, but we already have a workstation setting,
384 // etc. for this one. Just manually remove it from the API params.
385 delete apiParams['auto_print_holds_transits'];
390 checkout(params: CheckoutParams): Promise<CheckoutResult> {
392 params.new_copy_alerts = true;
393 params._renewal = false;
394 console.debug('checking out with', params);
396 let method = 'open-ils.circ.checkout.full';
397 if (params._override) { method += '.override'; }
399 return this.inspectBarcode(params).then(barcodeOk => {
400 if (!barcodeOk) { return null; }
402 return this.net.request(
403 'open-ils.circ', method,
404 this.auth.token(), this.apiParams(params)).toPromise()
405 .then(result => this.unpackCheckoutData(params, result))
406 .then(result => this.processCheckoutResult(result));
410 renew(params: CheckoutParams): Promise<CheckoutResult> {
412 params.new_copy_alerts = true;
413 params._renewal = true;
414 console.debug('renewing out with', params);
416 let method = 'open-ils.circ.renew';
417 if (params._override) { method += '.override'; }
419 return this.inspectBarcode(params).then(barcodeOk => {
420 if (!barcodeOk) { return null; }
422 return this.net.request(
423 'open-ils.circ', method,
424 this.auth.token(), this.apiParams(params)).toPromise()
425 .then(result => this.unpackCheckoutData(params, result))
426 .then(result => this.processCheckoutResult(result));
432 params: CheckoutParams, response: any): Promise<CheckoutResult> {
434 const allEvents = Array.isArray(response) ?
435 response.map(r => this.evt.parse(r)) :
436 [this.evt.parse(response)];
438 console.debug('checkout events', allEvents.map(e => e.textcode));
439 console.debug('checkout returned', allEvents);
441 const firstEvent = allEvents[0];
442 const payload = firstEvent.payload;
444 const result: CheckoutResult = {
445 index: CircService.resultIndex++,
446 firstEvent: firstEvent,
447 allEvents: allEvents,
452 // Some scenarios (e.g. copy in transit) have no payload,
454 if (!payload) { return Promise.resolve(result); }
456 result.circ = payload.circ;
457 result.copy = payload.copy;
458 result.volume = payload.volume;
459 result.patron = payload.patron;
460 result.record = payload.record;
461 result.nonCatCirc = payload.noncat_circ;
463 return this.fleshCommonData(result).then(_ => {
464 const action = params._renewal ? 'renew' :
465 (params.noncat ? 'noncat_checkout' : 'checkout');
466 this.addWorkLog(action, result);
471 processCheckoutResult(result: CheckoutResult): Promise<CheckoutResult> {
472 const renewing = result.params._renewal;
473 const key = renewing ? 'renew' : 'checkout';
475 const overridable = renewing ?
476 CAN_OVERRIDE_RENEW_EVENTS : CAN_OVERRIDE_CHECKOUT_EVENTS;
478 if (result.allEvents.filter(
479 e => overridable.includes(e.textcode)).length > 0) {
480 return this.handleOverridableCheckoutEvents(result);
483 switch (result.firstEvent.textcode) {
485 result.success = true;
486 this.audio.play(`success.${key}`);
487 return Promise.resolve(result);
489 case 'ITEM_NOT_CATALOGED':
490 return this.handlePrecat(result);
492 case 'OPEN_CIRCULATION_EXISTS':
493 return this.handleOpenCirc(result);
495 case 'COPY_IN_TRANSIT':
496 this.audio.play(`warning.${key}.in_transit`);
497 return this.copyInTransitDialog(result);
499 case 'PATRON_CARD_INACTIVE':
500 case 'PATRON_INACTIVE':
501 case 'PATRON_ACCOUNT_EXPIRED':
502 case 'CIRC_CLAIMS_RETURNED':
503 case 'ACTOR_USER_NOT_FOUND':
504 case 'AVAIL_HOLD_COPY_RATIO_EXCEEDED':
505 this.audio.play(`warning.${key}`);
506 return this.exitAlert({
507 textcode: result.firstEvent.textcode,
508 barcode: result.params.copy_barcode
511 case 'ASSET_COPY_NOT_FOUND':
512 this.audio.play(`error.${key}.not_found`);
513 return this.exitAlert({
514 textcode: result.firstEvent.textcode,
515 barcode: result.params.copy_barcode
519 this.audio.play(`error.${key}.unknown`);
520 return this.exitAlert({
521 textcode: 'CHECKOUT_FAILED_GENERIC',
522 barcode: result.params.copy_barcode
527 exitAlert(context: any): Promise<any> {
528 const key = 'staff.circ.events.' + context.textcode;
529 return this.strings.interpolate(key, context)
531 this.components.circFailedDialog.dialogBody = str;
532 return this.components.circFailedDialog.open().toPromise();
534 .then(_ => Promise.reject('Bailling on event ' + context.textcode));
537 copyInTransitDialog(result: CheckoutResult): Promise<CheckoutResult> {
538 this.components.copyInTransitDialog.checkout = result;
540 return this.findCopyTransitByBarcode(result)
541 .then(_ => this.components.copyInTransitDialog.open().toPromise())
542 .then(cancelAndCheckout => {
543 if (cancelAndCheckout) {
545 return this.abortTransit(result.transit.id())
547 // We had to look up the copy from the barcode since
548 // it was not embedded in the result event. Since
549 // we have the specifics on the copy, go ahead and
550 // copy them into the params we use for the follow
552 result.params.copy_barcode = result.copy.barcode();
553 result.params.copy_id = result.copy.id();
554 return this.checkout(result.params);
563 // Ask the user if we should resolve the circulation and check
564 // out to the user or leave it alone.
565 // When resolving and checking out, renew if it's for the same
566 // user, otherwise check it in, then back out to the current user.
567 handleOpenCirc(result: CheckoutResult): Promise<CheckoutResult> {
569 let sameUser = false;
571 return this.net.request(
573 'open-ils.circ.copy_checkout_history.retrieve',
574 this.auth.token(), result.params.copy_id, 1).toPromise()
577 const circ = circs[0];
579 sameUser = result.params.patron_id === circ.usr();
580 this.components.openCircDialog.sameUser = sameUser;
581 this.components.openCircDialog.circDate = circ.xact_start();
583 return this.components.openCircDialog.open().toPromise();
586 .then(fromDialog => {
588 // Leave the open circ checked out.
589 if (!fromDialog) { return result; }
591 const coParams = Object.assign({}, result.params); // clone
594 coParams.void_overdues = fromDialog.forgiveFines;
595 return this.renew(coParams);
598 const ciParams: CheckinParams = {
600 copy_id: coParams.copy_id,
601 void_overdues: fromDialog.forgiveFines
604 return this.checkin(ciParams)
607 return this.checkout(coParams);
609 return Promise.reject('Unable to check in item');
615 handleOverridableCheckoutEvents(result: CheckoutResult): Promise<CheckoutResult> {
616 const params = result.params;
617 const firstEvent = result.firstEvent;
618 const events = result.allEvents;
620 if (params._override) {
621 // Should never get here. Just being safe.
622 return Promise.reject(null);
626 e => !this.autoOverrideCheckoutEvents[e.textcode]).length === 0) {
627 // User has already seen all of these events and overridden them,
628 // so avoid showing them again since they are all auto-overridable.
629 params._override = true;
630 return params._renewal ? this.renew(params) : this.checkout(params);
633 // New-style alerts are reported via COPY_ALERT_MESSAGE and
634 // includes the alerts in the payload as an array.
635 if (firstEvent.textcode === 'COPY_ALERT_MESSAGE'
636 && Array.isArray(firstEvent.payload)) {
637 this.components.copyAlertManager.alerts = firstEvent.payload;
639 this.components.copyAlertManager.mode =
640 params._renewal ? 'renew' : 'checkout';
642 return this.components.copyAlertManager.open().toPromise()
645 params._override = true;
646 return this.checkout(params);
651 return this.showOverrideDialog(result, events);
654 showOverrideDialog(result: CheckoutResult,
655 events: EgEvent[], checkin?: boolean): Promise<CheckoutResult> {
657 const params = result.params;
658 const mode = checkin ? 'checkin' : (params._renewal ? 'renew' : 'checkout');
660 const holdShelfEvent = events.filter(e => e.textcode === 'ITEM_ON_HOLDS_SHELF')[0];
662 if (holdShelfEvent) {
663 this.components.circEventsDialog.clearHolds = this.clearHoldsOnCheckout;
664 this.components.circEventsDialog.patronId = holdShelfEvent.payload.patron_id;
665 this.components.circEventsDialog.patronName = holdShelfEvent.payload.patron_name;
668 this.components.circEventsDialog.copyBarcode = result.params.copy_barcode;
669 this.components.circEventsDialog.events = events;
670 this.components.circEventsDialog.mode = mode;
672 return this.components.circEventsDialog.open().toPromise()
674 const confirmed = resp.override;
675 if (!confirmed) { return null; }
677 let promise = Promise.resolve(null);
680 // Indicate these events have been seen and overridden.
681 events.forEach(evt => {
682 if (CHECKOUT_OVERRIDE_AFTER_FIRST.includes(evt.textcode)) {
683 this.autoOverrideCheckoutEvents[evt.textcode] = true;
687 if (holdShelfEvent && resp.clearHold) {
688 const holdId = holdShelfEvent.payload.hold_id;
690 // Cancel the hold that put our checkout item
691 // on the holds shelf.
693 promise = promise.then(_ => {
694 return this.net.request(
696 'open-ils.circ.hold.cancel',
700 'Item checked out by other patron' // FIXME I18n
706 return promise.then(_ => {
707 params._override = true;
708 return this[mode](params); // checkout/renew/checkin
713 handlePrecat(result: CheckoutResult): Promise<CheckoutResult> {
714 this.components.precatDialog.barcode = result.params.copy_barcode;
716 return this.components.precatDialog.open().toPromise().then(values => {
718 if (values && values.dummy_title) {
719 const params = result.params;
720 params.precat = true;
721 Object.keys(values).forEach(key => params[key] = values[key]);
722 return this.checkout(params);
725 result.canceled = true;
726 return Promise.resolve(result);
730 checkin(params: CheckinParams): Promise<CheckinResult> {
731 params.new_copy_alerts = true;
733 console.debug('checking in with', params);
735 let method = 'open-ils.circ.checkin';
736 if (params._override) { method += '.override'; }
738 return this.inspectBarcode(params).then(barcodeOk => {
739 if (!barcodeOk) { return null; }
741 return this.net.request(
742 'open-ils.circ', method,
743 this.auth.token(), this.apiParams(params)).toPromise()
744 .then(result => this.unpackCheckinData(params, result))
745 .then(result => this.processCheckinResult(result));
749 fleshCommonData(result: CircResultCommon): Promise<CircResultCommon> {
751 const copy = result.copy;
752 const volume = result.volume;
753 const circ = result.circ;
754 const hold = result.hold;
755 const nonCatCirc = (result as CheckoutResult).nonCatCirc;
757 let promise = Promise.resolve();
759 if (!result.patron) {
762 patronId = hold.usr();
764 patronId = circ.usr();
765 } else if (nonCatCirc) {
766 patronId = nonCatCirc.patron();
770 promise = promise.then(_ => {
771 return this.pcrud.retrieve('au', patronId,
772 {flesh: 1, flesh_fields : {'au' : ['card']}})
773 .toPromise().then(p => result.patron = p);
780 result.title = result.record.title();
781 result.author = result.record.author();
782 result.isbn = result.record.isbn();
785 result.title = result.copy.dummy_title();
786 result.author = result.copy.dummy_author();
787 result.isbn = result.copy.dummy_isbn();
791 if (this.copyLocationCache[copy.location()]) {
792 copy.location(this.copyLocationCache[copy.location()]);
794 promise = this.pcrud.retrieve('acpl', copy.location()).toPromise()
797 this.copyLocationCache[loc.id()] = loc;
801 if (typeof copy.status() !== 'object') {
802 promise = promise.then(_ => this.holdings.getCopyStatuses())
805 Object.values(stats).filter(s => s.id() === copy.status())[0];
806 if (stat) { copy.status(stat); }
812 // Flesh volume prefixes and suffixes
814 if (typeof volume.prefix() !== 'object') {
815 promise = promise.then(_ =>
816 this.pcrud.retrieve('acnp', volume.prefix()).toPromise()
817 ).then(p => volume.prefix(p));
820 if (typeof volume.suffix() !== 'object') {
821 promise = promise.then(_ =>
822 this.pcrud.retrieve('acns', volume.suffix()).toPromise()
823 ).then(p => volume.suffix(p));
827 return promise.then(_ => result);
830 unpackCheckinData(params: CheckinParams, response: any): Promise<CheckinResult> {
831 const allEvents = Array.isArray(response) ?
832 response.map(r => this.evt.parse(r)) : [this.evt.parse(response)];
834 console.debug('checkin events', allEvents.map(e => e.textcode));
835 console.debug('checkin response', response);
837 const firstEvent = allEvents[0];
838 const payload = firstEvent.payload;
841 firstEvent.textcode.match(/SUCCESS|NO_CHANGE|ROUTE_ITEM/) !== null;
843 const result: CheckinResult = {
844 index: CircService.resultIndex++,
845 firstEvent: firstEvent,
846 allEvents: allEvents,
852 // e.g. ASSET_COPY_NOT_FOUND
853 return Promise.resolve(result);
856 result.circ = payload.circ;
857 result.parent_circ = payload.parent_circ;
858 result.copy = payload.copy;
859 result.volume = payload.volume;
860 result.record = payload.record;
861 result.transit = payload.transit;
862 result.hold = payload.hold;
864 const copy = result.copy;
865 const volume = result.volume;
866 const transit = result.transit;
867 const circ = result.circ;
868 const parent_circ = result.parent_circ;
871 if (typeof transit.dest() !== 'object') {
872 transit.dest(this.org.get(transit.dest()));
874 if (typeof transit.source() !== 'object') {
875 transit.source(this.org.get(transit.source()));
879 // for checkin, the mbts lives on the main circ
880 if (circ && circ.billable_transaction()) {
881 result.mbts = circ.billable_transaction().summary();
884 // on renewals, the mbts lives on the parent circ
885 if (parent_circ && parent_circ.billable_transaction()) {
886 result.mbts = parent_circ.billable_transaction().summary();
889 return this.fleshCommonData(result).then(_ => {
890 this.addWorkLog('checkin', result);
895 processCheckinResult(result: CheckinResult): Promise<CheckinResult> {
896 const params = result.params;
897 const allEvents = result.allEvents;
899 // Informational alerts that can be ignored if configured.
900 if (this.suppressCheckinPopups &&
901 allEvents.filter(e =>
902 !CAN_SUPPRESS_CHECKIN_ALERTS.includes(e.textcode)).length === 0) {
904 // Should not be necessary, but good to be safe.
905 if (params._override) { return Promise.resolve(null); }
907 params._override = true;
908 return this.checkin(params);
911 // Alerts that require a manual override.
912 if (allEvents.filter(
913 e => CAN_OVERRIDE_CHECKIN_ALERTS.includes(e.textcode)).length > 0) {
914 return this.handleOverridableCheckinEvents(result);
917 switch (result.firstEvent.textcode) {
920 return this.handleCheckinSuccess(result);
922 case 'ITEM_NOT_CATALOGED':
923 this.audio.play('error.checkout.no_cataloged');
924 result.routeTo = this.components.catalogingStr.text;
925 return this.showPrecatAlert().then(_ => result);
928 this.audio.play(result.hold ?
929 'info.checkin.transit.hold' : 'info.checkin.transit');
932 console.debug('Skipping route dialog on "noop" checkin');
933 return Promise.resolve(result);
936 this.components.routeDialog.checkin = result;
937 return this.findCopyTransit(result)
938 .then(_ => this.components.routeDialog.open().toPromise())
941 case 'ASSET_COPY_NOT_FOUND':
942 this.audio.play('error.checkin.not_found');
943 return this.handleCheckinUncatAlert(result);
946 this.audio.play('error.checkin.unknown');
948 'Unhandled checkin response : ' + result.firstEvent.textcode);
951 return Promise.resolve(result);
954 addWorkLog(action: string, result: CircResultCommon) {
955 const params = result.params;
956 const copy = result.copy;
957 const patron = result.patron;
959 // Some worklog data may be provided by the caller in the params.
960 const entry: WorkLogEntry =
961 Object.assign(params._worklog || {}, {action: action});
964 entry.item = copy.barcode();
965 entry.item_id = copy.id();
967 entry.item = params.copy_barcode;
968 entry.item_id = params.copy_id;
972 entry.patron_id = patron.id();
973 entry.user = patron.family_name();
977 entry.hold_id = result.hold.id();
980 this.worklog.record(entry);
983 showPrecatAlert(): Promise<any> {
984 if (!this.suppressCheckinPopups && !this.ignoreCheckinPrecats) {
985 // Tell the user its a precat and return the result.
986 return this.components.routeToCatalogingDialog.open()
989 return Promise.resolve(null);
992 handleCheckinSuccess(result: CheckinResult): Promise<CheckinResult> {
993 const copy = result.copy;
995 if (!copy) { return Promise.resolve(result); }
997 const stat = copy.status();
998 const statId = typeof stat === 'object' ? stat.id() : stat;
1002 case 0: /* AVAILABLE */
1003 case 4: /* MISSING */
1004 case 7: /* RESHELVING */
1005 this.audio.play('success.checkin');
1006 return this.handleCheckinLocAlert(result);
1008 case 8: /* ON HOLDS SHELF */
1009 this.audio.play('info.checkin.holds_shelf');
1011 const hold = result.hold;
1015 if (Number(hold.pickup_lib()) === Number(this.auth.user().ws_ou())) {
1016 result.routeTo = this.components.holdShelfStr.text;
1017 this.components.routeDialog.checkin = result;
1018 return this.components.routeDialog.open().toPromise()
1022 // Should not happen in practice, but to be safe.
1023 this.audio.play('warning.checkin.wrong_shelf');
1027 console.warn('API Returned insufficient info on holds');
1031 case 11: /* CATALOGING */
1032 this.audio.play('info.checkin.cataloging');
1033 result.routeTo = this.components.catalogingStr.text;
1034 return this.showPrecatAlert().then(_ => result);
1036 case 15: /* ON_RESERVATION_SHELF */
1037 this.audio.play('info.checkin.reservation');
1041 this.audio.play('success.checkin');
1042 console.debug(`Unusual checkin copy status (may have been
1043 set via copy alert): status=${statId}`);
1046 return Promise.resolve(result);
1049 handleCheckinLocAlert(result: CheckinResult): Promise<CheckinResult> {
1050 const copy = result.copy;
1052 if (this.suppressCheckinPopups
1053 || copy.location().checkin_alert() === 'f') {
1054 return Promise.resolve(result);
1057 return this.strings.interpolate(
1058 'staff.circ.checkin.location.alert',
1059 {barcode: copy.barcode(), location: copy.location().name()}
1061 this.components.locationAlertDialog.dialogBody = str;
1062 return this.components.locationAlertDialog.open().toPromise()
1067 handleCheckinUncatAlert(result: CheckinResult): Promise<CheckinResult> {
1068 const barcode = result.copy ?
1069 result.copy.barcode() : result.params.copy_barcode;
1071 if (this.suppressCheckinPopups) {
1072 return Promise.resolve(result);
1075 return this.strings.interpolate(
1076 'staff.circ.checkin.uncat.alert', {barcode: barcode}
1078 this.components.uncatAlertDialog.dialogBody = str;
1079 return this.components.uncatAlertDialog.open().toPromise()
1085 handleOverridableCheckinEvents(result: CheckinResult): Promise<CheckinResult> {
1086 const params = result.params;
1087 const events = result.allEvents;
1088 const firstEvent = result.firstEvent;
1090 if (params._override) {
1091 // Should never get here. Just being safe.
1092 return Promise.reject(null);
1095 if (this.suppressCheckinPopups && events.filter(
1096 e => !CAN_SUPPRESS_CHECKIN_ALERTS.includes(e.textcode)).length === 0) {
1097 // These events are automatically overridden when suppress
1098 // popups are in effect.
1099 params._override = true;
1100 return this.checkin(params);
1103 // New-style alerts are reported via COPY_ALERT_MESSAGE and
1104 // includes the alerts in the payload as an array.
1105 if (firstEvent.textcode === 'COPY_ALERT_MESSAGE'
1106 && Array.isArray(firstEvent.payload)) {
1107 this.components.copyAlertManager.alerts = firstEvent.payload;
1108 this.components.copyAlertManager.mode = 'checkin';
1110 return this.components.copyAlertManager.open().toPromise()
1113 if (!resp) { return result; } // dialog was canceled
1115 if (resp.nextStatus !== null) {
1116 params.next_copy_status = [resp.nextStatus];
1117 params.capture = 'nocapture';
1120 params._override = true;
1122 return this.checkin(params);
1126 return this.showOverrideDialog(result, events, true);
1130 // The provided params (minus the copy_id) will be used
1132 checkoutBatch(copyIds: number[],
1133 params: CheckoutParams): Observable<CheckoutResult> {
1135 if (copyIds.length === 0) { return empty(); }
1137 return from(copyIds).pipe(concatMap(id => {
1138 const cparams = Object.assign({}, params); // clone
1139 cparams.copy_id = id;
1140 return from(this.checkout(cparams));
1144 // The provided params (minus the copy_id) will be used
1146 renewBatch(copyIds: number[],
1147 params?: CheckoutParams): Observable<CheckoutResult> {
1149 if (copyIds.length === 0) { return empty(); }
1150 if (!params) { params = {}; }
1152 return from(copyIds).pipe(concatMap(id => {
1153 const cparams = Object.assign({}, params); // clone
1154 cparams.copy_id = id;
1155 return from(this.renew(cparams));
1159 // The provided params (minus the copy_id) will be used
1161 checkinBatch(copyIds: number[],
1162 params?: CheckinParams): Observable<CheckinResult> {
1164 if (copyIds.length === 0) { return empty(); }
1165 if (!params) { params = {}; }
1167 return from(copyIds).pipe(concatMap(id => {
1168 const cparams = Object.assign({}, params); // clone
1169 cparams.copy_id = id;
1170 return from(this.checkin(cparams));
1174 abortTransit(transitId: number): Promise<any> {
1175 return this.net.request(
1177 'open-ils.circ.transit.abort',
1178 this.auth.token(), {transitid : transitId}
1179 ).toPromise().then(resp => {
1180 const evt = this.evt.parse(resp);
1183 return Promise.reject(evt.toString());
1185 return Promise.resolve();
1189 lastCopyCirc(copyId: number): Promise<IdlObject> {
1190 return this.pcrud.search('circ',
1191 {target_copy : copyId},
1192 {order_by : {circ : 'xact_start desc' }, limit : 1}
1196 // Resolves to true if the barcode is OK or the user confirmed it or
1197 // the user doesn't care to begin with
1198 inspectBarcode(params: CheckoutParams | CheckinParams): Promise<boolean> {
1199 if (!params._checkbarcode || !params.copy_barcode) {
1200 return Promise.resolve(true);
1203 if (this.checkBarcode(params.copy_barcode)) {
1204 // Avoid prompting again on an override
1205 params._checkbarcode = false;
1206 return Promise.resolve(true);
1209 this.components.badBarcodeDialog.barcode = params.copy_barcode;
1210 return this.components.badBarcodeDialog.open().toPromise()
1211 // Avoid prompting again on an override
1213 params._checkbarcode = false
1218 checkBarcode(barcode: string): boolean {
1219 if (barcode !== Number(barcode).toString()) { return false; }
1221 const bc = barcode.toString();
1223 // "16.00" == Number("16.00"), but the . is bad.
1224 // Throw out any barcode that isn't just digits
1225 if (bc.search(/\D/) !== -1) { return false; }
1227 const lastDigit = bc.substr(bc.length - 1);
1228 const strippedBarcode = bc.substr(0, bc.length - 1);
1229 return this.barcodeCheckdigit(strippedBarcode).toString() === lastDigit;
1232 barcodeCheckdigit(bc: string): number {
1235 const reverseBarcode = bc.toString().split('').reverse();
1237 reverseBarcode.forEach(ch => {
1239 const product = (Number(ch) * multiplier) + '';
1240 product.split('').forEach(num => tempSum += Number(num));
1241 checkSum += Number(tempSum);
1242 multiplier = multiplier === 2 ? 1 : 2;
1245 const cSumStr = checkSum.toString();
1246 const nextMultipleOf10 =
1247 (Number(cSumStr.match(/(\d*)\d$/)[1]) * 10) + 10;
1249 let checkDigit = nextMultipleOf10 - Number(cSumStr);
1250 if (checkDigit === 10) { checkDigit = 0; }