# Pass second argument of '1' to enable template caching.
use OpenILS::WWW::PrintTemplate ('@sysconfdir@/opensrf_core.xml', 0);
+use OpenILS::WWW::SIP2Mediator ('@sysconfdir@/opensrf_core.xml');
+
# - Uncomment the following 2 lines to make use of the IP redirection code
# - The IP file should to contain a map with the following format:
# - actor.org_unit.shortname <start_ip> <end_ip>
Require all granted
</Location>
+<Location /sip2-mediator>
+ SetHandler perl-script
+ PerlHandler OpenILS::WWW::SIP2Mediator
+ Options +ExecCGI
+ Require all granted
+</Location>
+
# OpenURL 0.1 searching based on OpenSearch
RewriteMap openurl prg:@bindir@/openurl_map.pl
RewriteCond %{QUERY_STRING} (^.*$)
<field reporter:label="Transaction Closed?" name="xact_closed" sr:suggest_filter="true" reporter:datatype="bool"/>
</fields>
</class>
+ <class id="sipsetg" controller="open-ils.cstore open-ils.pcrud"
+ oils_obj:fieldmapper="sip::setting_group"
+ oils_persist:tablename="sip.setting_group"
+ reporter:label="SIP Settings Group">
+ <fields oils_persist:primary="id" oils_persist:sequence="sip.setting_group_id_seq">
+ <field name="id" reporter:datatype="id" reporter:label="ID" reporter:selector="label"/>
+ <field name="label" reporter:datatype="text" reporter:label="Label" oils_obj:required="true"/>
+ <field name="institution" reporter:datatype="text" reporter:label="SIP Institution" oils_obj:required="true"/>
+ <field name="settings" reporter:datatype="link" reporter:label="Settings" oils_persist:virtual="true"/>
+ </fields>
+ <links>
+ <link field="settings" reltype="has_many" key="setting_group" map="" class="sipset"/>
+ </links>
+ <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+ <actions>
+ <create permission="SIP_ADMIN" global_required="true"/>
+ <retrieve permission="SIP_ADMIN" global_required="true"/>
+ <update permission="SIP_ADMIN" global_required="true"/>
+ <delete permission="SIP_ADMIN" global_required="true"/>
+ </actions>
+ </permacrud>
+ </class>
+ <class id="sipset" controller="open-ils.cstore open-ils.pcrud"
+ oils_obj:fieldmapper="sip::setting"
+ oils_persist:tablename="sip.setting"
+ reporter:label="SIP Settings">
+ <fields oils_persist:primary="id" oils_persist:sequence="sip.setting_id_seq">
+ <field name="id" reporter:datatype="id" reporter:label="ID" reporter:selector="name"/>
+ <field name="setting_group" reporter:datatype="link" reporter:label="Settings Group" oils_obj:required="true" config_field="true"/>
+ <field name="name" reporter:datatype="text" reporter:label="Name" oils_obj:required="true"/>
+ <field name="description" reporter:datatype="text" reporter:label="Description" oils_obj:required="true"/>
+ <field name="value" reporter:datatype="text" reporter:label="Value" oils_obj:required="true"/>
+ </fields>
+ <links>
+ <link field="setting_group" reltype="has_a" key="id" map="" class="sipsetg"/>
+ </links>
+ <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+ <actions>
+ <create permission="SIP_ADMIN" global_required="true"/>
+ <retrieve permission="SIP_ADMIN" global_required="true"/>
+ <update permission="SIP_ADMIN" global_required="true"/>
+ <delete permission="SIP_ADMIN" global_required="true"/>
+ </actions>
+ </permacrud>
+ </class>
+ <class id="sipacc" controller="open-ils.cstore open-ils.pcrud"
+ oils_obj:fieldmapper="sip::account"
+ oils_persist:tablename="sip.account"
+ reporter:label="SIP Account">
+ <fields oils_persist:primary="id" oils_persist:sequence="sip.account_id_seq">
+ <field name="id" reporter:datatype="id" reporter:label="ID" reporter:selector="sip_username"/>
+ <field name="enabled" reporter:datatype="bool" reporter:label="Enabled"/>
+ <field name="setting_group" reporter:datatype="link" reporter:label="Settings Group" oils_obj:required="true"/>
+ <field name="sip_username" reporter:datatype="text" reporter:label="SIP Username" oils_obj:required="true"/>
+ <field name="usr" reporter:datatype="link" reporter:label="ILS User" oils_obj:required="true"/>
+ <field name="workstation" reporter:datatype="link" reporter:label="Workstation"/>
+ <field name="transient" reporter:datatype="bool" reporter:label="Transient"/>
+ <field name="activity_who" reporter:datatype="text" reporter:label="Activity Who"/>
+ <field name="sip_password" reporter:datatype="id" reporter:label="SIP Password" oils_persist:virtual="true"/>
+ </fields>
+ <links>
+ <link field="usr" reltype="has_a" key="id" map="" class="au"/>
+ <link field="workstation" reltype="has_a" key="id" map="" class="aws"/>
+ <link field="setting_group" reltype="has_a" key="id" map="" class="sipsetg"/>
+ </links>
+ <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+ <actions>
+ <create permission="SIP_ADMIN" global_required="true"/>
+ <retrieve permission="SIP_ADMIN" global_required="true"/>
+ <update permission="SIP_ADMIN" global_required="true"/>
+ <delete permission="SIP_ADMIN" global_required="true"/>
+ </actions>
+ </permacrud>
+ </class>
+ <class id="sipses" controller="open-ils.cstore"
+ oils_obj:fieldmapper="sip::session"
+ oils_persist:tablename="sip.session"
+ reporter:label="SIP Session">
+ <fields oils_persist:primary="key">
+ <field name="key" reporter:datatype="text" reporter:label="SIP Session Key"/>
+ <field name="ils_token" reporter:datatype="text" reporter:label="ILS Auth Token"/>
+ <field name="account" reporter:datatype="link" reporter:label="SIP Account"/>
+ <field name="create_time" reporter:label="Create Time" reporter:datatype="timestamp"/>
+ </fields>
+ <links>
+ <link field="account" reltype="has_a" key="id" map="" class="sipacc"/>
+ </links>
+ </class>
+ <class id="sipsm" controller="open-ils.cstore open-ils.pcrud"
+ oils_obj:fieldmapper="sip::screen_message"
+ oils_persist:tablename="sip.screen_message"
+ reporter:label="SIP Screen Message">
+ <fields oils_persist:primary="key">
+ <field name="key" reporter:datatype="text" reporter:label="Message Key"/>
+ <field name="message" reporter:datatype="text" reporter:label="Message" oils_persist:i18n="true"/>
+ </fields>
+ <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+ <actions>
+ <create permission="SIP_ADMIN" global_required="true"/>
+ <retrieve />
+ <update permission="SIP_ADMIN" global_required="true"/>
+ <delete permission="SIP_ADMIN" global_required="true"/>
+ </actions>
+ </permacrud>
+ </class>
<class id="coauf"
controller="open-ils.cstore open-ils.pcrud"
</unix_config>
</open-ils.courses>
+ <open-ils.sip2>
+ <keepalive>5</keepalive>
+ <stateless>1</stateless>
+ <language>perl</language>
+ <implementation>OpenILS::Application::SIP2</implementation>
+ <max_requests>100</max_requests>
+ <unix_config>
+ <unix_sock>sip2_unix.sock</unix_sock>
+ <unix_pid>sip2_unix.pid</unix_pid>
+ <unix_log>sip2_unix.log</unix_log>
+ <max_requests>1000</max_requests>
+ <min_children>1</min_children>
+ <max_children>15</max_children>
+ <min_spare_children>1</min_spare_children>
+ <max_spare_children>5</max_spare_children>
+ </unix_config>
+ <app_settings>
+ </app_settings>
+ </open-ils.sip2>
+
<open-ils.curbside>
<keepalive>5</keepalive>
<stateless>1</stateless>
<app_settings>
</app_settings>
</open-ils.curbside>
+
</apps>
</default>
<appname>open-ils.courses</appname>
<appname>open-ils.curbside</appname>
<appname>open-ils.geo</appname>
+ <appname>open-ils.sip2</appname>
</activeapps>
</localhost>
</hosts>
<service>open-ils.vandelay</service>
<service>open-ils.serial</service>
<service>open-ils.ebook_api</service>
+ <service>open-ils.sip2</service>
</services>
</router>
<div class="modal-body">
<p>{{dialogBody}}</p>
<div class="text-center">
- <input class="form-control" [(ngModel)]="promptValue"/>
+ <input type="{{promptType}}" class="form-control" [(ngModel)]="promptValue"/>
</div>
</div>
<div class="modal-footer">
@Input() public dialogBody: string;
// Value to return to the caller
@Input() public promptValue: string;
+ // 'password', etc.
+ @Input() promptType = 'text';
}
@Input() hideBanner: boolean;
// do not close dialog on error saving record
- @Input() remainOpenOnError: false;
+ @Input() remainOpenOnError = false;
+
+ // Avoid making any pcrud calls. Instead return the modified object
+ // to the caller via recordSaved Output and dialog close().
+ @Input() inPlaceMode = false;
// Emit the modified object when the save action completes.
@Output() recordSaved = new EventEmitter<IdlObject>();
// custom function for munging the record before it gets saved;
// will get passed mode and the record itself
- @Input() preSave: Function;
+ @Input() preSave: (mode: string, recToSave: IdlObject) => void;
// recordId and record getters and setters.
// Note that setting the this.recordId to NULL does not clear the
this.preSave(this.mode, recToSave);
}
this.convertDatatypesToIdl(recToSave);
+
+ if (this.inPlaceMode) {
+ this.recordSaved.emit(recToSave);
+ if (this.fmEditForm) {
+ this.fmEditForm.form.markAsPristine();
+ }
+ if (this.isDialog()) {
+ this.record = undefined;
+ this.close(recToSave);
+ }
+ return;
+ }
+
this.pcrud[this.mode]([recToSave]).toPromise().then(
result => {
this.recordSaved.emit(result);
routerLink="/staff/admin/server/config/remote_account"></eg-link-table-link>
<eg-link-table-link i18n-label label="Remote Authentication Profiles"
routerLink="/staff/admin/server/config/remoteauth_profile"></eg-link-table-link>
+ <eg-link-table-link i18n-label label="SIP Accounts"
+ routerLink="/staff/admin/server/sip/account"></eg-link-table-link>
+ <eg-link-table-link i18n-label label="SIP Screen Messages"
+ routerLink="/staff/admin/server/sip/screen_message"></eg-link-table-link>
<eg-link-table-link i18n-label label="SMS Carriers"
routerLink="/staff/admin/server/config/sms_carrier"></eg-link-table-link>
<eg-link-table-link i18n-label label="User Activity Types"
data: [{schema: 'asset',
table: 'call_number_suffix', readonlyFields: 'label_sortkey'}]
}, {
+ path: 'sip/account',
+ loadChildren: () =>
+ import('./sip/account.module').then(m => m.SipAccountModule)
+}, {
+ path: 'sip/screen_message',
+ component: BasicAdminPageComponent,
+ data: [{schema: 'sip',
+ table: 'screen_message', readonlyFields: 'key'}]
+}, {
path: ':schema/:table',
component: BasicAdminPageComponent
}];
--- /dev/null
+<eg-staff-banner bannerText="SIP Accounts" i18n-bannerText></eg-staff-banner>
+
+<eg-confirm-dialog #confirmDelete
+ i18n-dialogTitle i18n-dialogBody
+ dialogTitle="Delete Accounts"
+ dialogBody="Delete selected SIP accounts?">
+</eg-confirm-dialog>
+
+<eg-grid #grid idlClass="sipacc" [dataSource]="gridSource"
+ persistKey="admin.server.sip.account-list"
+ [stickyHeader]="true" [sortable]="true" (onRowActivate)="openAccount($event)">
+ <eg-grid-toolbar-button label="New Account" i18n-label
+ (onClick)="newAccount()"></eg-grid-toolbar-button>
+ <eg-grid-toolbar-action label="Delete Selected" i18n-label
+ (onClick)="deleteSelected($event)">
+ </eg-grid-toolbar-action>
+</eg-grid>
+
--- /dev/null
+import {Component, Input, ViewChild, OnInit} from '@angular/core';
+import {Router} from '@angular/router';
+import {Observable, of} from 'rxjs';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {OrgService} from '@eg/core/org.service';
+import {AuthService} from '@eg/core/auth.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {ToastService} from '@eg/share/toast/toast.service';
+import {StringComponent} from '@eg/share/string/string.component';
+import {StringService} from '@eg/share/string/string.service';
+import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component';
+import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component';
+import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
+import {GridDataSource} from '@eg/share/grid/grid';
+import {GridComponent} from '@eg/share/grid/grid.component';
+import {Pager} from '@eg/share/util/pager';
+
+@Component({
+ templateUrl: './account-list.component.html'
+})
+export class SipAccountListComponent implements OnInit {
+
+ gridSource: GridDataSource = new GridDataSource();
+ @ViewChild('grid') grid: GridComponent;
+ @ViewChild('confirmDelete') confirmDelete: ConfirmDialogComponent;
+
+ constructor(
+ private router: Router,
+ private pcrud: PcrudService
+ ) {}
+
+ ngOnInit() {
+ this.gridSource.getRows = (pager: Pager, sort: any[]) => {
+ return this.fetchAccounts(pager, sort);
+ };
+ }
+
+ fetchAccounts(pager: Pager, sort: any[]): Observable<any> {
+
+ const orderBy: any = {sisacc: 'sip_username'};
+ if (sort.length) {
+ orderBy.sisacc = sort[0].name + ' ' + sort[0].dir;
+ }
+
+ return this.pcrud.retrieveAll('sipacc', {
+ offset: pager.offset,
+ limit: pager.limit,
+ order_by: orderBy,
+ flesh: 1,
+ flesh_fields: {sipacc: ['usr', 'setting_group', 'workstation']}
+ });
+ }
+
+ openAccount(row: any) {
+ this.router.navigate([`/staff/admin/server/sip/account/${row.id()}`]);
+ }
+
+ newAccount() {
+ this.router.navigate([`/staff/admin/server/sip/account/new`]);
+ }
+
+ deleteSelected(rows: any[]) {
+ if (rows.length === 0) { return; }
+
+ this.confirmDelete.open().subscribe(confirmed => {
+ if (confirmed) {
+ rows.forEach(row => row.isdeleted(true));
+ this.pcrud.autoApply(rows).toPromise().then(_ => {
+ this.gridSource.reset();
+ this.grid.reload();
+ });
+ }
+ });
+ }
+}
+
--- /dev/null
+<eg-staff-banner bannerText="SIP Account" i18n-bannerText></eg-staff-banner>
+
+<a routerLink="/staff/admin/server/sip/account">
+ <button class="btn btn-outline-dark label-with-material-icon">
+ <span class="material-icons">reply</span>
+ <span i18n>SIP Accounts</span>
+ </button>
+</a>
+
+<eg-sip-group-delete-dialog *ngIf="account && account.setting_group()"
+ #deleteGroupDialog
+ [group]="account.setting_group()" [settingGroups]="settingGroups">
+</eg-sip-group-delete-dialog>
+
+<eg-fm-record-editor #cloneDialog idlClass="sipsetg" mode="create"
+ hiddenFields="id" fieldOrder="label,institution">
+</eg-fm-record-editor>
+
+<eg-fm-record-editor #settingDialog idlClass="sipset" mode="update"
+ hiddenFields="id,setting_group" fieldOrder="name,description,value"
+ [fieldOptions]="{name:{isReadonly:true}}">
+</eg-fm-record-editor>
+
+<ng-container *ngIf="createMode">
+ <eg-prompt-dialog #passwordDialog i18n-dialogTitle i18n-dialogBody
+ dialogTitle="Create SIP Password" i18n-dialogBody="Create a new password"
+ promptType="password">
+ </eg-prompt-dialog>
+</ng-container>
+
+<ng-container *ngIf="!createMode">
+ <eg-prompt-dialog #passwordDialog i18n-dialogTitle i18n-dialogBody
+ dialogTitle="Create SIP Password" i18n-dialogBody="Create a new password"
+ promptType="password">
+ </eg-prompt-dialog>
+</ng-container>
+
+<div class="row mt-2" *ngIf="account">
+ <div class="col-lg-7">
+
+ <ng-template #usrTemplate>
+ <eg-combobox #usrCbox [entries]="usrCboxEntries" required="true"
+ (onChange)="usrChanged($event)"
+ [selectedId]="usrId" [asyncDataSource]="usrCboxSource">
+ </eg-combobox>
+ </ng-template>
+
+ <ng-template #grpTemplate>
+ <div class="form-inline">
+ <eg-combobox #grpCbox required="true"
+ [selectedId]="account.setting_group() ? account.setting_group().id() : null"
+ [entries]="settingGroups" (onChange)="grpChanged($event)">
+ </eg-combobox>
+ <button class="btn btn-outline-info ml-2"
+ [disabled]="!account.setting_group()" (click)="openCloneDialog()"
+ i18n>Clone</button>
+ <button class="btn btn-outline-danger ml-2" (click)="openDeleteDialog()"
+ [disabled]="!account.setting_group() || account.setting_group().id() == 1"
+ i18n>Delete</button>
+ </div>
+ </ng-template>
+
+ <ng-template #sipUsernameTemplate>
+ <div class="form-inline">
+ <input type="text" class="form-control"
+ [ngModel]="account.sip_username()"
+ (ngModelChange)="account.sip_username($event)">
+ <button class="btn btn-outline-dark ml-2"
+ [ngClass]="{'border-danger' : createMode && !account.sip_password()}"
+ (click)="setPassword()">Set Password</button>
+ </div>
+ </ng-template>
+
+ <eg-fm-record-editor #editor
+ idlClass="sipacc" [mode]="createMode ? 'create' : 'update'"
+ hiddenFields="id" displayMode="inline" [inPlaceMode]="true"
+ fieldOrder="sip_username,sip_password,setting_group,usr,workstation,transient,activity_who,enabled"
+ [fieldOptions]="{
+ setting_group:{customTemplate:{template:grpTemplate}},
+ sip_username:{customTemplate:{template:sipUsernameTemplate}},
+ usr:{customTemplate:{template:usrTemplate}}}"
+ [preSave]="accountPreSave" [recordId]="!createMode ? accountId : null"
+ (recordSaved)="accountSaved($event)">
+ </eg-fm-record-editor>
+ </div>
+ <div class="col-lg-5">
+ <ul>
+ <li i18n>Save account changes before modifying individual settings.</li>
+ <li i18n>Setting values must be entered as valid JSON.</li>
+ <li i18n>The "Default Settings" group cannot be modified.</li>
+ <li i18n>The same "SIP Institution" value may be used for multiple groups.</li>
+ </ul>
+ </div>
+</div>
+
+<div class="row" *ngIf="account && account.setting_group()">
+
+ <div class="col-lg-12 border-top mt-2 pt-2">
+ <h4 class="mb-2" i18n>Settings For Group
+ <span class="font-weight-bold">'{{account.setting_group().label()}}'</span>
+ </h4>
+
+ <eg-grid #settingGrid idlClass="sipset" [dataSource]="settingsSource"
+ [sortable]="true" (onRowActivate)="editSetting($event)"
+ persistKey="admin.server.sip.account.settings" hideFields="id,setting_group">
+ <eg-grid-toolbar-action label="Edit Selected" i18n-label
+ (onClick)="editFirstSetting($event)">
+ </eg-grid-toolbar-action>
+ </eg-grid>
+ </div>
+</div>
+
--- /dev/null
+import {Component, Input, ViewChild, OnInit} from '@angular/core';
+import {Router, ActivatedRoute} from '@angular/router';
+import {Observable, of} from 'rxjs';
+import {map, tap, switchMap, catchError} from 'rxjs/operators';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {OrgService} from '@eg/core/org.service';
+import {NetService} from '@eg/core/net.service';
+import {AuthService} from '@eg/core/auth.service';
+import {EventService} from '@eg/core/event.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {ToastService} from '@eg/share/toast/toast.service';
+import {StringComponent} from '@eg/share/string/string.component';
+import {StringService} from '@eg/share/string/string.service';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+import {PromptDialogComponent} from '@eg/share/dialog/prompt.component';
+import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component';
+import {ComboboxEntry, ComboboxComponent} from '@eg/share/combobox/combobox.component';
+import {GridComponent} from '@eg/share/grid/grid.component';
+import {GridDataSource} from '@eg/share/grid/grid';
+import {Pager} from '@eg/share/util/pager';
+
+@Component({
+ templateUrl: './account.component.html'
+})
+export class SipAccountComponent implements OnInit {
+
+ accountId: number;
+ account: IdlObject;
+ usrCboxSource: (term: string) => Observable<ComboboxEntry>;
+ usrCboxEntries: ComboboxEntry[];
+ settingGroups: ComboboxEntry[];
+ usrId: number;
+ settingsSource: GridDataSource = new GridDataSource();
+ deleteGroupAccounts: IdlObject[] = [];
+ accountPreSave: (mode: string, account: IdlObject) => void;
+ createMode = false;
+
+ @ViewChild('cloneDialog') cloneDialog: FmRecordEditorComponent;
+ @ViewChild('settingDialog') settingDialog: FmRecordEditorComponent;
+ @ViewChild('settingGrid') settingGrid: GridComponent;
+ @ViewChild('deleteGroupDialog') deleteGroupDialog: DialogComponent;
+ @ViewChild('passwordDialog') passwordDialog: PromptDialogComponent;
+
+ constructor(
+ private route: ActivatedRoute,
+ private router: Router,
+ private idl: IdlService,
+ private net: NetService,
+ private auth: AuthService,
+ private evt: EventService,
+ private pcrud: PcrudService
+ ) {}
+
+ ngOnInit() {
+
+ this.route.paramMap.subscribe(params => {
+ if (params.get('id') === 'new') {
+ this.account = this.idl.create('sipacc'); // dummy
+ this.createMode = true;
+ return;
+ }
+
+ this.accountId = Number(params.get('id'));
+ this.loadAccount().toPromise(); // force it to run
+ });
+
+ this.fetchGroups();
+
+ this.usrCboxSource = term => {
+
+ const filter: any = {deleted: 'f', active: 't'};
+
+ if (this.account && this.account.usr()) {
+ filter['-or'] = [
+ {id: this.account.usr().id()},
+ {usrname: {'ilike': `%${term}%`}}
+ ];
+ } else {
+ filter.usrname = {'ilike': `%${term}%`};
+ }
+
+ return this.pcrud.search('au', filter, {
+ order_by: {au: 'usrname'},
+ limit: 50 // Avoid huge lists
+ }
+ ).pipe(map(user => {
+ return {id: user.id(), label: user.usrname()};
+ }));
+ };
+
+ this.settingsSource.getRows = (pager: Pager, sort: any[]) => {
+ if (!this.account && this.account.setting_group()) {
+ return of();
+ }
+
+ const orderBy: any = {sipset: 'name'};
+ if (sort.length) {
+ orderBy.sipset = sort[0].name + ' ' + sort[0].dir;
+ }
+
+ return this.pcrud.search('sipset',
+ {setting_group: this.account.setting_group().id()},
+ {order_by: orderBy},
+ );
+ };
+
+ this.accountPreSave = (mode: string, account: IdlObject) => {
+ // Migrate data collected from custom templates into
+ // the object to be saved.
+ account.setting_group(this.account.setting_group().id());
+ account.usr(this.account.usr().id());
+ account.sip_username(this.account.sip_username());
+ account.sip_password(this.account.sip_password());
+ };
+ }
+
+ fetchGroups() {
+ this.pcrud.retrieveAll('sipsetg',
+ {order_by: {sipsetg: 'label'}}, {atomic: true})
+ .subscribe(grps => {
+ this.settingGroups =
+ grps.map(g => ({id: g.id(), label: g.label()}));
+ });
+ }
+
+ loadAccount(): Observable<any> {
+ return this.pcrud.retrieve('sipacc', this.accountId, {
+ flesh: 2,
+ flesh_fields: {
+ sipacc: ['usr', 'setting_group', 'workstation'],
+ sipsetg: ['settings']
+ }}, {authoritative: true}
+ ).pipe(tap(acc => {
+ this.account = acc;
+ this.usrId = acc.usr().id();
+ this.usrCboxEntries =
+ [{id: acc.usr().id(), label: acc.usr().usrname()}];
+ }));
+ }
+
+ grpChanged(entry: ComboboxEntry) {
+
+ if (!entry) {
+ this.account.setting_group(null);
+ return;
+ }
+
+ this.pcrud.retrieve('sipsetg', entry.id,
+ {flesh: 1, flesh_fields: {sipsetg: ['settings']}})
+ .subscribe(grp => {
+ this.account.setting_group(grp);
+ if (this.settingGrid) {
+ this.settingGrid.reload();
+ }
+ });
+ }
+
+ usrChanged(entry: ComboboxEntry) {
+ if (!entry) {
+ this.account.usr(null);
+ return;
+ }
+
+ this.pcrud.retrieve('au', entry.id)
+ .subscribe(usr => this.account.usr(usr));
+ }
+
+
+ // Create a new setting group
+ // Clone the settings for the currently selected group into the new group
+ // Point our account at the new group.
+ openCloneDialog() {
+ this.cloneDialog.open().subscribe(resp => {
+ if (!resp) { return; }
+
+ this.settingGroups.unshift({id: resp.id(), label: resp.label()});
+
+ const settings = this.account.setting_group().settings()
+ .map(setting => {
+ const clone = this.idl.clone(setting);
+ clone.setting_group(resp.id());
+ clone.isnew(true);
+ clone.id(null);
+ return clone;
+ });
+
+ // avoid de-fleshing the group on the active account
+ const modified = this.idl.clone(this.account);
+ modified.setting_group(resp.id());
+ modified.ischanged(true);
+
+ this.pcrud.autoApply(settings.concat(modified)).toPromise()
+ .then(_ => this.refreshAccount());
+ });
+ }
+
+ openDeleteDialog() {
+ this.deleteGroupDialog.open().subscribe(
+ ok => {
+ if (ok) {
+ this.refreshAccount();
+ }
+ }
+ );
+ }
+
+ accountSaved(account) {
+
+ if (this.createMode) {
+ account.isnew(true);
+ } else {
+ account.ischanged(true);
+ }
+
+ this.net.request('open-ils.sip2',
+ 'open-ils.sip2.account.cud', this.auth.token(), account)
+ .subscribe(acc => {
+
+ const evt = this.evt.parse(acc);
+
+ if (evt) {
+ console.error(evt);
+ return;
+ }
+
+ if (this.createMode) {
+ this.router.navigate(
+ [`/staff/admin/server/sip/account/${acc.id()}`]);
+ } else {
+ this.refreshAccount();
+ }
+ });
+ }
+
+ editFirstSetting(rows: any) {
+ if (rows.length > 0) {
+ this.editSetting(rows[0]);
+ }
+ }
+
+ refreshAccount() {
+ this.loadAccount().subscribe(_ => {
+ setTimeout(() => {
+ if (this.settingGrid) {
+ this.settingGrid.reload();
+ }
+ });
+ });
+ }
+
+ editSetting(row: any) {
+ // Default Settings group is read-only
+ if (row.setting_group() === 1) { return; }
+
+ this.settingDialog.record = this.idl.clone(row);
+ this.settingDialog.open().subscribe(
+ ok => this.refreshAccount(),
+ err => {} // todo toast
+ );
+ }
+
+
+ setPassword() {
+ this.passwordDialog.open().subscribe(value => {
+ // API will translate this into an actor.passwd
+ this.account.sip_password(value);
+ });
+ }
+}
+
--- /dev/null
+import {NgModule} from '@angular/core';
+import {AdminCommonModule} from '@eg/staff/admin/common.module';
+import {SipAccountRoutingModule} from './routing.module';
+import {SipAccountListComponent} from './account-list.component';
+import {SipAccountComponent} from './account.component';
+import {DeleteGroupDialogComponent} from './delete-group-dialog.component';
+
+@NgModule({
+ declarations: [
+ SipAccountComponent,
+ SipAccountListComponent,
+ DeleteGroupDialogComponent
+ ],
+ imports: [
+ AdminCommonModule,
+ SipAccountRoutingModule
+ ],
+ exports: [
+ ],
+ providers: [
+ ]
+})
+
+export class SipAccountModule {
+}
+
+
--- /dev/null
+<ng-template #dialogContent>
+ <div class="modal-header bg-info">
+ <h4 class="modal-title" i18n>Delete Setting Group</h4>
+ <button type="button" class="close"
+ i18n-aria-label aria-label="Close" (click)="close()">
+ <span aria-hidden="true">×</span>
+ </button>
+ </div>
+ <div class="modal-body">
+ <div class="row">
+ <div class="col-lg-12" *ngIf="group" i18n>
+ Deleting SIP setting group: {{group.label()}}
+ </div>
+ </div>
+ <div class="row mt-2 pt-2 border-top">
+ <div class="col-lg-12" i18n>
+ Select another group to act as the transfer target group.
+ Any SIP accounts linked to the deleted group will be transferred
+ to this group:
+ </div>
+ </div>
+ <div class="row mt-2 pt-2 border-top">
+ <div class="col-lg-8 offset-lg-2" i18n>
+ <eg-combobox #grpCbox required="true" [selectedId]="targetGroup"
+ [entries]="trimmedSettingGroups" (onChange)="grpChanged($event)">
+ </eg-combobox>
+ </div>
+ </div>
+
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-success"
+ (click)="doDelete(true)" i18n>Confirm</button>
+ <button type="button" class="btn btn-warning"
+ (click)="close(false)" i18n>Cancel</button>
+ </div>
+</ng-template>
--- /dev/null
+import {Component, Input, ViewChild, OnInit} from '@angular/core';
+import {Observable, of} from 'rxjs';
+import {map, tap, switchMap, catchError} from 'rxjs/operators';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {OrgService} from '@eg/core/org.service';
+import {AuthService} from '@eg/core/auth.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {NetService} from '@eg/core/net.service';
+import {ToastService} from '@eg/share/toast/toast.service';
+import {StringComponent} from '@eg/share/string/string.component';
+import {StringService} from '@eg/share/string/string.service';
+import {NgbModal, NgbModalRef, NgbModalOptions} from '@ng-bootstrap/ng-bootstrap';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component';
+import {ComboboxEntry, ComboboxComponent} from '@eg/share/combobox/combobox.component';
+import {GridComponent} from '@eg/share/grid/grid.component';
+import {GridDataSource} from '@eg/share/grid/grid';
+import {Pager} from '@eg/share/util/pager';
+
+@Component({
+ templateUrl: './delete-group-dialog.component.html',
+ selector: 'eg-sip-group-delete-dialog'
+})
+export class DeleteGroupDialogComponent extends DialogComponent implements OnInit {
+
+ @Input() group: IdlObject;
+ @Input() settingGroups: ComboboxEntry[];
+ targetGroup = 1; // Default to the 'Default Settings' group.
+ trimmedSettingGroups: ComboboxEntry[];
+
+ constructor(
+ private modal: NgbModal,
+ private auth: AuthService,
+ private net: NetService
+ ) {
+ super(modal);
+ }
+
+ ngOnInit() {
+ this.onOpen$.subscribe(_ => {
+ this.trimmedSettingGroups = this.settingGroups.filter(
+ entry => entry.id !== this.group.id());
+ });
+ }
+
+ grpChanged(entry: ComboboxEntry) {
+ if (entry) {
+ this.targetGroup = entry.id;
+ }
+ }
+
+ doDelete() {
+ this.net.request('open-ils.sip2',
+ 'open-ils.sip2.setting_group.delete',
+ this.auth.token(), this.group.id(), this.targetGroup
+ ).subscribe(ok => this.close((Number(ok) === 1)));
+ }
+}
+
--- /dev/null
+import {NgModule} from '@angular/core';
+import {RouterModule, Routes} from '@angular/router';
+import {SipAccountListComponent} from './account-list.component';
+import {SipAccountComponent} from './account.component';
+
+const routes: Routes = [{
+ path: '',
+ component: SipAccountListComponent
+}, {
+ path: ':id',
+ component: SipAccountComponent
+}];
+
+@NgModule({
+ imports: [RouterModule.forChild(routes)],
+ exports: [RouterModule]
+})
+
+export class SipAccountRoutingModule {}
+
--- /dev/null
+package OpenILS::Application::SIP2;
+use strict; use warnings;
+use base 'OpenILS::Application';
+use DateTime;
+use DateTime::Format::ISO8601;
+use OpenILS::Application;
+use OpenILS::Event;
+use OpenILS::Utils::Fieldmapper;
+use OpenSRF::Utils::Logger qw(:logger);
+use OpenILS::Utils::CStoreEditor qw/:funcs/;
+use OpenILS::Application::AppUtils;
+use OpenILS::Utils::DateTime qw/:datetime/;
+use OpenILS::Application::SIP2::Common;
+use OpenILS::Application::SIP2::Session;
+use OpenILS::Application::SIP2::Item;
+use OpenILS::Application::SIP2::Hold;
+use OpenILS::Application::SIP2::Patron;
+use OpenILS::Application::SIP2::Checkout;
+use OpenILS::Application::SIP2::Checkin;
+use OpenILS::Application::SIP2::Payment;
+use OpenILS::Application::SIP2::Admin;
+
+my $U = 'OpenILS::Application::AppUtils';
+my $SC = 'OpenILS::Application::SIP2::Common';
+
+__PACKAGE__->register_method(
+ method => 'dispatch_sip2_request',
+ api_name => 'open-ils.sip2.request',
+ api_level => 1,
+ argc => 2,
+ signature => {
+ desc => q/
+ Takes a SIP2 JSON message and handles the request/,
+ params => [{
+ name => 'seskey',
+ desc => 'The SIP2 session key',
+ type => 'string'
+ }, {
+ name => 'message',
+ desc => 'SIP2 JSON message',
+ type => q/SIP JSON object/
+ }],
+ return => {
+ desc => q/SIP2 JSON message on success, Event on error/,
+ type => 'object'
+ }
+ }
+);
+
+sub dispatch_sip2_request {
+ my ($self, $client, $seskey, $message) = @_;
+
+ OpenSRF::AppSession->ingress('sip2');
+
+ return OpenILS::Event->new('SIP2_SESSION_REQUIRED') unless $seskey;
+ my $msg_code = $message->{code};
+
+ return handle_login($seskey, $message) if $msg_code eq '93';
+ return handle_sc_status($seskey, $message) if $msg_code eq '99';
+
+ # A cached session means we have successfully logged in with
+ # the SIP credentials provided during a login request. All
+ # message types following require authentication.
+ my $session = OpenILS::Application::SIPSession->find($seskey);
+
+ if (!$session) {
+ return undef if $msg_code eq 'XS'; # end session signal
+ return OpenILS::Event->new('SIP2_SESSION_REQUIRED');
+ }
+
+ my $MESSAGE_MAP = {
+ '09' => \&handle_checkin,
+ '11' => \&handle_checkout,
+ '15' => \&handle_hold,
+ '17' => \&handle_item_info,
+ '23' => \&handle_patron_status,
+ '29' => \&handle_renew,
+ '37' => \&handle_payment,
+ '63' => \&handle_patron_info,
+ '65' => \&handle_renew_all,
+ 'XS' => \&handle_end_session
+ };
+
+ return OpenILS::Event->new('SIP2_NOT_IMPLEMENTED', {payload => $message})
+ unless exists $MESSAGE_MAP->{$msg_code};
+
+ return $MESSAGE_MAP->{$msg_code}->($session, $message);
+}
+
+sub handle_end_session {
+ my ($session, $message) = @_;
+ my $e = $session->editor;
+ my $seskey = $session->seskey;
+
+ $SC->cache->delete_cache("sip2_$seskey");
+
+ $U->simplereq('open-ils.auth',
+ 'open-ils.auth.session.delete', $e->authtoken);
+
+ return undef if $U->is_true($session->sip_account->transient);
+
+ $e->xact_begin;
+ my $ses = $e->retrieve_sip_session($seskey);
+ if ($ses) {
+ $e->delete_sip_session($ses);
+ $e->commit;
+ } else {
+ $e->rollback;
+ }
+
+ return undef;
+}
+
+# Login to Evergreen and cache the login data.
+sub handle_login {
+ my ($seskey, $message) = @_;
+ my $e = new_editor();
+
+ # Default to login-failed
+ my $response = {code => '94', fixed_fields => ['0']};
+
+ my $sip_username = $SC->get_field_value($message, 'CN');
+ my $sip_password = $SC->get_field_value($message, 'CO');
+ my $sip_account = $e->search_sip_account([
+ {sip_username => $sip_username, enabled => 't'},
+ {flesh => 1, flesh_fields => {sipacc => ['workstation']}}
+ ])->[0];
+
+ if (!$sip_account) {
+ $logger->warn("SIP2: No such SIP account: $sip_username");
+ return $response;
+ }
+
+ if ($U->verify_user_password($e, $sip_account->usr, $sip_password, 'sip2')) {
+
+ my $session = OpenILS::Application::SIPSession->new(
+ seskey => $seskey,
+ sip_account => $sip_account
+ );
+ $response->{fixed_fields}->[0] = '1' if $session->set_ils_account;
+
+ } else {
+ $logger->info("SIP2: login failed for user=$sip_username")
+ }
+
+ return $response;
+}
+
+sub handle_sc_status {
+ my ($seskey, $message) = @_;
+
+ my $session = OpenILS::Application::SIPSession->find($seskey);
+
+ my $config;
+
+ if ($session) {
+ $config = $session->config;
+
+ } else {
+
+ # Confirm sc-status-before-login is enabled before continuing.
+
+ my $flag = new_editor()->search_config_global_flag({
+ name => 'sip.sc_status_before_login_institution',
+ value => {'!=' => undef},
+ enabled => 't',
+ })->[0];
+
+ return OpenILS::Event->new(
+ 'SC_STATUS_REQUIRES_LOGIN', {payload => $message}) unless $flag;
+
+ $config = {
+ settings => {},
+ id => $flag->value,
+ supports => OpenILS::Application::SIPSession->supports
+ };
+ }
+
+ my $response = {
+ code => '98',
+ fixed_fields => [
+ $SC->sipbool(1), # online_status
+ $SC->sipbool(1), # checkin_ok
+ $SC->sipbool(1), # checkout_ok
+ $SC->sipbool(1), # acs_renewal_policy
+ $SC->sipbool(0), # status_update_ok
+ $SC->sipbool(0), # offline_ok
+ '999', # timeout_period
+ '999', # retries_allowed
+ $SC->sipdate, # transaction date
+ '2.00' # protocol_version
+ ],
+ fields => [
+ {AO => $config->{institution}},
+ {BX => join('', @{$config->{supports}})}
+ ]
+ }
+}
+
+sub handle_item_info {
+ my ($session, $message) = @_;
+
+ my $barcode = $SC->get_field_value($message, 'AB');
+ my $config = $session->config;
+
+ my $details = OpenILS::Application::SIP2::Item->get_item_details(
+ $session, barcode => $barcode
+ );
+
+ if (!$details) {
+ # No matching item found, return a minimal response.
+ return {
+ code => '18',
+ fixed_fields => [
+ '01', # circ status: other/Unknown
+ '01', # security marker: other/unknown
+ '01', # fee type: other/unknown
+ $SC->sipdate
+ ],
+ fields => [{AB => $barcode, AJ => ''}]
+ };
+ };
+
+ return {
+ code => '18',
+ fixed_fields => [
+ $details->{circ_status},
+ '02', # Security Marker, consistent with ../SIP*
+ $details->{fee_type},
+ $SC->sipdate
+ ],
+ fields => [
+ {AB => $barcode},
+ {AH => $details->{due_date}},
+ {AJ => $details->{title}},
+ {AP => $details->{current_loc}},
+ {AQ => $details->{permanent_loc}},
+ {BG => $details->{owning_loc}},
+ {BH => $config->{settings}->{currency}},
+ {BV => $details->{item}->deposit_amount},
+ {CF => $details->{hold_queue_length}},
+ {CK => $details->{media_type}},
+ {CM => $details->{hold_pickup_date}},
+ {CT => $details->{destination_loc}},
+ {CY => $details->{hold_patron_barcode}}
+ ]
+ };
+}
+
+sub handle_patron_info {
+ my ($session, $message) = @_;
+ my $sip_account = $session->sip_account;
+
+ my $barcode = $SC->get_field_value($message, 'AA');
+ my $password = $SC->get_field_value($message, 'AD');
+
+ my $summary =
+ ref $message->{fixed_fields} ? $message->{fixed_fields}->[2] : '';
+
+ my $list_items = $SC->patron_summary_list_items($summary);
+
+ my $details = OpenILS::Application::SIP2::Patron->get_patron_details(
+ $session,
+ barcode => $barcode,
+ password => $password,
+ summary_start_item => $SC->get_field_value($message, 'BP'),
+ summary_end_item => $SC->get_field_value($message, 'BQ'),
+ summary_list_items => $list_items
+ );
+
+ my $response =
+ patron_response_common_data($session, $barcode, $password, $details);
+
+ $response->{code} = '64';
+
+ return $response unless $details;
+ my $patron = $details->{patron};
+
+ push(
+ @{$response->{fixed_fields}},
+ $SC->count4($details->{holds_count}),
+ $SC->count4($details->{overdue_count}),
+ $SC->count4($details->{out_count}),
+ $SC->count4($details->{fine_count}),
+ $SC->count4($details->{recall_count}),
+ $SC->count4($details->{unavail_holds_count})
+ );
+
+ push(
+ @{$response->{fields}},
+ {BE => $patron->email},
+ {PA => $SC->sipymd($patron->expire_date)},
+ {PB => $SC->sipymd($patron->dob, 1)},
+ {PC => $patron->profile->name},
+ {XI => $patron->id}
+ );
+
+ if ($list_items eq 'hold_items') {
+ for my $hold (@{$details->{hold_items}}) {
+ push(@{$response->{fields}}, {AS => $hold});
+ }
+ } elsif ($list_items eq 'charged_items') {
+ for my $item (@{$details->{items_out}}) {
+ push(@{$response->{fields}}, {AU => $item});
+ }
+ } elsif ($list_items eq 'overdue_items') {
+ for my $item (@{$details->{overdue_items}}) {
+ push(@{$response->{fields}}, {AT => $item});
+ }
+ } elsif ($list_items eq 'fine_items') {
+ for my $item (@{$details->{fine_items}}) {
+ push(@{$response->{fields}}, {AV => $item});
+ }
+ } elsif ($list_items eq 'unavailable_holds') {
+ for my $item (@{$details->{unavailable_holds}}) {
+ push(@{$response->{fields}}, {CD => $item});
+ }
+ }
+
+ # NOTE: Recall Items (BU) is not supported.
+
+ return $response;
+}
+
+sub handle_patron_status {
+ my ($session, $message) = @_;
+ my $sip_account = $session->sip_account;
+
+ my $barcode = $SC->get_field_value($message, 'AA');
+ my $password = $SC->get_field_value($message, 'AD');
+
+ my $details = OpenILS::Application::SIP2::Patron->get_patron_details(
+ $session,
+ barcode => $barcode,
+ password => $password
+ );
+
+ my $response = patron_response_common_data(
+ $session, $barcode, $password, $details);
+
+ $response->{code} = '24';
+
+ return $response;
+}
+
+# Patron Info and Patron Status responses share mostly the same data.
+# This returns the base data which can be augmented as needed.
+# Note we don't call Patron->get_patron_details here since different
+# messages collect different amounts of data.
+sub patron_response_common_data {
+ my ($session, $barcode, $password, $details) = @_;
+
+ if (!$details) {
+ # No such user. Return a stub response with all things denied.
+
+ return {
+ fixed_fields => [
+ $SC->spacebool(1), # charge denied
+ $SC->spacebool(1), # renew denied
+ $SC->spacebool(1), # recall denied
+ $SC->spacebool(1), # holds denied
+ split('', (' ' x 10)),
+ '000', # language
+ $SC->sipdate
+ ],
+ fields => [
+ {AO => $session->config->{institution}},
+ {AA => $barcode},
+ {BL => $SC->sipbool(0)}, # valid patron
+ {CQ => $SC->sipbool(0)} # valid patron password
+ ]
+ };
+ }
+
+ my $patron = $details->{patron};
+
+ return {
+ fixed_fields => [
+ $SC->spacebool($details->{charge_denied}),
+ $SC->spacebool($details->{renew_denied}),
+ $SC->spacebool($details->{recall_denied}),
+ $SC->spacebool($details->{holds_denied}),
+ $SC->spacebool($patron->card->active eq 'f'),
+ $SC->spacebool(0), # too many charged
+ $SC->spacebool($details->{too_may_overdue}),
+ $SC->spacebool(0), # too many renewals
+ $SC->spacebool(0), # too many claims retruned
+ $SC->spacebool(0), # too many lost
+ $SC->spacebool($details->{too_many_fines}),
+ $SC->spacebool($details->{too_many_fines}),
+ $SC->spacebool(0), # recall overdue
+ $SC->spacebool($details->{too_many_fines}),
+ '000', # language
+ $SC->sipdate
+ ],
+ fields => [
+ {AA => $barcode},
+ {AO => $session->config->{institution}},
+ {BH => $session->config->{settings}->{currency}},
+ {BL => $SC->sipbool(1)}, # valid patron
+ {BV => $details->{balance_owed}}, # fee amount
+ {CQ => $SC->sipbool($password)} # password verified if exists
+ ]
+ };
+}
+
+
+sub handle_checkout {
+ my ($session, $message) = @_;
+ return checkout_renew_common($session, $message);
+}
+
+sub handle_renew {
+ my ($session, $message) = @_;
+ return checkout_renew_common($session, $message, 1);
+}
+
+sub checkout_renew_common {
+ my ($session, $message, $is_renewal) = @_;
+ my $config = $session->config;
+
+ my $patron_barcode = $SC->get_field_value($message, 'AA');
+ my $item_barcode = $SC->get_field_value($message, 'AB');
+ my $fee_ack = $SC->get_field_value($message, 'BO');
+
+ my $code = $is_renewal ? '30' : '12';
+ my $stub = {
+ code => $code,
+ fixed_fields => [
+ 0, # checkout ok
+ $SC->sipbool(0), # renewal ok
+ $SC->sipbool(0), # magnetic media
+ $SC->sipbool(0), # desensitize
+ $SC->sipdate, # transaction date
+ ],
+ fields => [
+ {AA => $patron_barcode},
+ {AB => $item_barcode}
+ ]
+ };
+
+ my $item_details = OpenILS::Application::SIP2::Item->get_item_details(
+ $session, barcode => $item_barcode);
+
+ return $stub unless $item_details;
+
+ my $patron_details = OpenILS::Application::SIP2::Patron->get_patron_details(
+ $session, barcode => $patron_barcode);
+
+ return $stub unless $patron_details;
+
+ my $circ_details = OpenILS::Application::SIP2::Checkout->checkout(
+ $session,
+ patron_barcode => $patron_barcode,
+ item_barcode => $item_barcode,
+ fee_ack => $fee_ack,
+ is_renew => $is_renewal
+ );
+
+ my $magnetic = $item_details->{magnetic_media};
+ my $deposit = $item_details->{item}->deposit_amount;
+ my $screen_msg = $circ_details->{screen_msg};
+ my $due_date = $circ_details->{due_date};
+ my $circ = $circ_details->{circ};
+
+ my $can_renew = 0;
+ if ($circ) {
+ $can_renew = !$patron_details->{renew_denied}
+ && $circ->renewal_remaining > 0;
+ }
+
+ return {
+ code => $code,
+ fixed_fields => [
+ $circ ? 1 : 0, # checkout ok
+ $SC->sipbool($can_renew), # renewal ok
+ $SC->sipbool($magnetic), # magnetic media
+ $SC->sipbool(!$magnetic), # desensitize
+ $SC->sipdate, # transaction date
+ ],
+ fields => [
+ {AA => $patron_barcode},
+ {AB => $item_barcode},
+ {AJ => $item_details->{title}},
+ {AO => $config->{institution}},
+ {BT => $item_details->{fee_type}},
+ {CI => 0}, # security inhibit
+ {CK => $item_details->{media_type}},
+ $screen_msg ? {AF => $screen_msg} : (),
+ $due_date ? {AH => $due_date} : (),
+ $circ ? {BK => $circ->id} : (),
+ $deposit ? {BV => $deposit} : (),
+ ]
+ };
+}
+
+sub handle_renew_all {
+ my ($session, $message) = @_;
+ my $config = $session->config;
+
+ my $patron_barcode = $SC->get_field_value($message, 'AA');
+ my $fee_ack = $SC->get_field_value($message, 'BO');
+
+ my $stub = {
+ code => '66',
+ fixed_fields => [
+ 0, # ok
+ $SC->count4(0), # renewed count
+ $SC->count4(0), # unrenewed count
+ $SC->sipdate, # transaction date
+ ],
+ fields => [
+ {AA => $patron_barcode},
+ {AO => $config->{institution}},
+ ]
+ };
+
+ my $patron_details = OpenILS::Application::SIP2::Patron->get_patron_details(
+ $session, barcode => $patron_barcode);
+
+ return $stub unless $patron_details;
+
+ my $circ_details = OpenILS::Application::SIP2::Checkout->renew_all(
+ $session, $patron_details, fee_ack => $fee_ack
+ );
+
+ my $screen_msg = $circ_details->{screen_msg};
+ my @renewed = @{$circ_details->{items_renewed}};
+ my @unrenewed = @{$circ_details->{items_unrenewed}};
+
+ return {
+ code => '66',
+ fixed_fields => [
+ 1, # ok
+ $SC->count4(scalar(@renewed)),
+ $SC->count4(scalar(@unrenewed)),
+ $SC->sipdate,
+ ],
+ fields => [
+ {AA => $patron_barcode},
+ {AO => $config->{institution}},
+ @{ [ map { {BM => $_} } @renewed ] },
+ @{ [ map { {BN => $_} } @unrenewed ] },
+ $screen_msg ? {AF => $screen_msg} : ()
+ ]
+ };
+}
+
+
+sub handle_checkin {
+ my ($session, $message) = @_;
+ my $config = $session->config;
+
+ my @fixed_fields = @{$message->{fixed_fields} || []};
+
+ my $item_barcode = $SC->get_field_value($message, 'AB');
+ my $current_loc = $SC->get_field_value($message, 'AP');
+ my $return_date = $fixed_fields[2];
+
+ my $stub = {
+ code => '10',
+ fixed_fields => [
+ 0, # checkin ok
+ $SC->sipbool(0), # resensitize
+ $SC->sipbool(0), # magnetic media
+ 'N', # alert
+ $SC->sipdate, # transaction date
+ ],
+ fields => [
+ {AB => $item_barcode},
+ {AO => $config->{institution}},
+ {CV => '00'} # unkown alert type
+ ]
+ };
+
+ my $item_details = OpenILS::Application::SIP2::Item->get_item_details(
+ $session, barcode => $item_barcode);
+
+ return $stub unless $item_details;
+
+ my $checkin_details = OpenILS::Application::SIP2::Checkin->checkin(
+ $session,
+ item_barcode => $item_barcode,
+ current_loc => $current_loc,
+ item_details => $item_details,
+ return_date => $return_date
+ );
+
+ my $screen_msg = $checkin_details->{screen_msg};
+ my $magnetic = $item_details->{magnetic_media};
+ my $hold_bc = $checkin_details->{hold_patron_barcode};
+ my $hold_name = $checkin_details->{hold_patron_name};
+ my $dest_loc = $checkin_details->{destination_loc};
+
+ return {
+ code => '10',
+ fixed_fields => [
+ $checkin_details->{ok}, # checkin ok
+ $SC->sipbool(!$magnetic), # resensitize
+ $SC->sipbool($magnetic), # magnetic media
+ $SC->sipbool($checkin_details->{alert}), # alert
+ $SC->sipdate, # transaction date
+ ],
+ fields => [
+ {AA => $checkin_details->{patron_barcode}},
+ {AB => $item_barcode},
+ {AJ => $item_details->{title}},
+ {AO => $config->{institution}},
+ {AP => $checkin_details->{current_loc}},
+ {AQ => $checkin_details->{permanent_loc}},
+ {BG => $item_details->{owning_loc}},
+ {BT => $item_details->{fee_type}},
+ {CI => 0}, # security inhibit
+ {CK => $item_details->{media_type}},
+ {CV => $checkin_details->{alert_type}},
+ $screen_msg ? {AF => $screen_msg} : (),
+ $dest_loc ? {CT => $dest_loc} : (),
+ $hold_bc ? {CY => $hold_bc} : (),
+ $hold_name ? {DA => $hold_name} : ()
+ ]
+ };
+}
+
+sub handle_hold {
+ my ($session, $message) = @_;
+ my $config = $session->config;
+
+ my @fixed_fields = @{$message->{fixed_fields} || []};
+ my $hold_mode = $fixed_fields[0];
+
+ my $patron_barcode = $SC->get_field_value($message, 'AA');
+ my $item_barcode = $SC->get_field_value($message, 'AB');
+
+ my $stub = {
+ code => '16',
+ fixed_fields => [
+ 0, # ok
+ 0, # available
+ $SC->sipdate
+ ],
+ fields => [
+ {AA => $patron_barcode},
+ {AB => $item_barcode},
+ {AO => $config->{institution}}
+ ]
+ };
+
+ # Hold Cancel is the only supported action.
+ return $stub unless $hold_mode eq '-';
+
+ my $patron_details =
+ OpenILS::Application::SIP2::Patron->get_patron_details(
+ $session, barcode => $patron_barcode);
+
+ return $stub unless $patron_details;
+
+ my $item_details = OpenILS::Application::SIP2::Item->get_item_details(
+ $session, barcode => $item_barcode);
+
+ return $stub unless $item_details;
+
+ my $hold = OpenILS::Application::SIP2::Hold->hold_from_copy(
+ $session, $patron_details, $item_details);
+
+ return $stub unless $hold;
+
+ my $details = OpenILS::Application::SIP2::Hold->cancel($session, $hold);
+
+ return $stub unless $details->{ok};
+
+ # report info on the targeted copy if one is set.
+ my $copy = $hold->current_copy || $item_details->{item};
+ my $title_id = $item_details->{item}->call_number->record->id;
+
+ return {
+ code => '16',
+ fixed_fields => [
+ 1, # ok
+ 0, # available
+ $SC->sipdate
+ ],
+ fields => [
+ {AA => $patron_barcode},
+ {AB => $copy->barcode},
+ {AJ => $title_id},
+ {AO => $config->{institution}}
+ ]
+ };
+}
+
+sub handle_payment {
+ my ($session, $message) = @_;
+ my $config = $session->config;
+
+ my @fixed_fields = @{$message->{fixed_fields} || []};
+
+ my $fee_type = $fixed_fields[1];
+ my $pay_type = $fixed_fields[2];
+ my $pay_amount = $SC->get_field_value($message, 'BV');
+ my $patron_barcode = $SC->get_field_value($message, 'AA');
+ my $fee_id = $SC->get_field_value($message, 'CG');
+ my $terminal_xact = $SC->get_field_value($message, 'BK');
+
+ # Envisionware extensions for relaying information about
+ # payments made via credit card kiosk or cash register.
+ my $register_login = $SC->get_field_value($message, 'OR');
+ my $check_number = $SC->get_field_value($message, 'RN');
+
+ my $details = OpenILS::Application::SIP2::Payment->apply_payment(
+ $session,
+ fee_id => $fee_id,
+ fee_type => $fee_type,
+ pay_type => $pay_type,
+ pay_amount => $pay_amount,
+ check_number => $check_number,
+ patron_barcode => $patron_barcode,
+ terminal_xact => $terminal_xact,
+ register_login => $register_login
+ );
+
+ my $screen_msg = $details->{screen_msg};
+
+ return {
+ code => '38',
+ fixed_fields => [
+ $SC->sipbool($details->{ok}),
+ $SC->sipdate,
+ ],
+ fields => [
+ {AA => $patron_barcode},
+ {AO => $config->{institution}},
+ $screen_msg ? {AF => $screen_msg} : (),
+ ]
+ }
+}
+
+1;
+
--- /dev/null
+package OpenILS::Application::SIP2::Admin;
+use strict; use warnings;
+use base 'OpenILS::Application';
+use OpenILS::Event;
+use OpenILS::Application;
+use OpenILS::Utils::Fieldmapper;
+use OpenSRF::Utils::Logger qw(:logger);
+use OpenILS::Utils::CStoreEditor qw/:funcs/;
+use OpenILS::Application::AppUtils;
+
+my $U = 'OpenILS::Application::AppUtils';
+
+__PACKAGE__->register_method(
+ method => 'delete_setting_group',
+ api_name => 'open-ils.sip2.setting_group.delete',
+ api_level => 1,
+ argc => 2,
+ signature => {
+ desc => q/
+ Takes a SIP2 JSON message and handles the request/,
+ params => [{
+ name => 'auth',
+ desc => 'Authtoken',
+ type => 'string'
+ }, {
+ name => 'del_grp_id',
+ desc => 'Setting group ID to delete',
+ type => 'number',
+ }, {
+ name => 'xfer_grp_id',
+ desc => q/Setting group ID to use as account transfer destination.
+ If no destination group is specified, defaults to setting
+ group ID 1 (Defaults)/,
+ type => 'number',
+ }],
+ return => {
+ desc => q/1 on success, Event on error/,
+ type => 'number | object'
+ }
+ }
+);
+
+sub delete_setting_group {
+ my ($self, $client, $auth, $del_grp_id, $xfer_grp_id) = @_;
+ $xfer_grp_id ||= 1; # Defaults Group
+
+ my $e = new_editor(authtoken => $auth, xact => 1);
+ return $e->die_event unless $e->checkauth;
+ return $e->die_event unless $e->allowed('SIP_ADMIN');
+
+ return $e->die_event unless
+ my $grp = $e->retrieve_sip_setting_group($del_grp_id);
+
+ my $accounts = $e->search_sip_account({setting_group => $del_grp_id});
+
+ for my $acc (@$accounts) {
+ $acc->setting_group($xfer_grp_id);
+ return $e->die_event unless $e->update_sip_account($acc);
+ }
+
+ # note: sip.setting objects are deleted via cascade
+ return $e->die_event
+ unless $e->delete_sip_setting_group($grp) && $e->commit;
+
+ return 1;
+}
+
+__PACKAGE__->register_method(
+ method => 'account_cud',
+ api_name => 'open-ils.sip2.account.cud',
+ api_level => 1,
+ argc => 2,
+ signature => {
+ desc => q/Create, Update, Delete SIP accounts. If a value is
+ stored in the virtual sip_password field on the account, the
+ value will be used as the new password for the account/,
+ params => [{
+ name => 'auth',
+ desc => 'Authtoken',
+ type => 'string'
+ }, {
+ name => 'account',
+ desc => 'SIP account object',
+ type => 'object'
+ }],
+ return => {
+ desc => q/Account object on success, Event on error/,
+ type => 'object'
+ }
+ }
+);
+
+sub account_cud {
+ my ($self, $client, $auth, $account) = @_;
+
+ my $e = new_editor(authtoken => $auth, xact => 1);
+ return $e->die_event unless $e->checkauth;
+ return $e->die_event unless $e->allowed('SIP_ADMIN');
+
+ if ($account->sip_password) {
+ my $pw = $e->json_query({from => ['actor.change_password',
+ $account->usr, $account->sip_password, 'sip2']});
+
+ return $e->die_event unless $pw;
+ }
+
+ if ($account->isnew) {
+ return undef unless $e->create_sip_account($account);
+
+ } elsif ($account->ischanged) {
+ return undef unless $e->update_sip_account($account);
+
+ } elsif ($account->isdeleted) {
+ return undef unless $e->delete_sip_account($account);
+ }
+
+ $account = $e->retrieve_sip_account($account->id);
+
+ return $e->die_event unless $e->commit;
+
+ return $account;
+}
+
+
+1;
--- /dev/null
+package OpenILS::Application::SIP2::Checkin;
+use strict; use warnings;
+use DateTime;
+use DateTime::Format::ISO8601;
+use OpenSRF::System;
+use OpenILS::Utils::CStoreEditor q/:funcs/;
+use OpenSRF::Utils::Logger q/$logger/;
+use OpenILS::Application::AppUtils;
+use OpenILS::Utils::DateTime qw/:datetime/;
+use OpenILS::Const qw/:const/;
+use OpenILS::Application::SIP2::Common;
+use OpenILS::Application::SIP2::Session;
+my $U = 'OpenILS::Application::AppUtils';
+my $SC = 'OpenILS::Application::SIP2::Common';
+
+
+sub checkin {
+ my ($class, $session, %params) = @_;
+
+ my $details = {};
+ my $override = 0;
+
+ for (0, 1) { # 2 checkin requests max
+
+ $override = perform_checkin($session, $details, $override, %params);
+
+ last unless $override;
+ }
+
+ return $details;
+}
+
+
+# Returns 1 if the checkin should be performed again with override.
+# Returns 0 if there's nothing left to do (final success / error)
+# Updates $details along the way.
+sub perform_checkin {
+ my ($session, $details, $override, %params) = @_;
+ my $config = $session->config;
+ my $item_details = $params{item_details};
+
+ my $args = {
+ copy_barcode => $params{item_barcode},
+ hold_as_transit => $config->{checkin_hold_as_transit}
+ };
+
+ if (my $backdate = $params{return_date}) {
+ $backdate =~ s/(\d{4})(\d{2})(\d{2}).*/$1-$2-$3/; # YYYYMMDD => ISO
+ $logger->info("Checking in with backdate $backdate");
+ $args->{backdate} = $backdate;
+ }
+
+ $args->{circ_lib} =
+ $SC->org_id_from_sn($session, $params{corrent_loc})
+ || $session->editor->requestor->ws_ou;
+
+ my $method = 'open-ils.circ.checkin';
+ $method .= '.override' if $override;
+
+ my $resp = $U->simplereq(
+ 'open-ils.circ', $method, $session->editor->authtoken, $args);
+
+ # Treat the first response as the main result.
+ my $event = ref $resp eq 'ARRAY' ? $resp->[0] : $resp;
+
+ return unless $U->is_event($event); # should never happen; fail gracefully
+
+ my $textcode = $event->{textcode};
+ my $payload = $event->{payload} || {};
+
+ return 1 if !$override && $config->{"checkin.override.$textcode"};
+
+ my $circ = $payload->{circ};
+ my $copy = $payload->{copy};
+
+ # These may be replaced below
+ $details->{current_loc} =
+ $params{item_details}->{item}->circ_lib->shortname;
+
+ $details->{permanent_loc} =
+ $params{item_details}->{item}->circ_lib->shortname;
+
+ $details->{destination_loc} =
+ $SC->org_sn_from_id($event->{org}) if $event->{org};
+
+ if ($copy && $copy->circ_lib != $item_details->{item}->circ_lib->id) {
+ # Checkin of floating copies changes the circ lib.
+ $details->{current_loc} =
+ $details->{permanent_loc} =
+ $SC->org_sn_from_id($session, $copy->circ_lib);
+ }
+
+ if ($circ) {
+ my $usr = $session->editor->retrieve_actor_user([
+ $circ->usr, {flesh => 1, flesh_fields => {au => ['card']}}]);
+
+ $details->{patron_barcode} =
+ $usr->card->barcode if $usr && $usr->card;
+ }
+
+ handle_hold($session, $details, $payload, %params);
+
+ if ($textcode eq 'NO_CHANGE' || $textcode eq 'SUCCESS') {
+
+ $details->{ok} = 1;
+
+ } elsif ($textcode eq 'ROUTE_ITEM') {
+
+ $details->{ok} = 1;
+ $details->{alert} = 1;
+ $details->{alert_type} = '04' unless $details->{alert_type};
+
+ } else {
+
+ $details->{ok} = 0; # unknown
+ $details->{alert} = 1;
+ $details->{alert_type} = '00' unless $details->{alert_type};
+ }
+
+ return 0;
+}
+
+sub handle_hold {
+ my ($session, $details, $payload, %params) = @_;
+
+ my $hold = $payload->{remote_hold} || $payload->{hold};
+
+ return unless $hold;
+
+ my ($pickup_lib_id, $pickup_lib_sn);
+
+ my $holder = $session->editor->retrieve_actor_user(
+ [$hold->usr, {flesh => 1, flesh_fields => {au => ['card']}}]);
+
+ $details->{hold_patron_name} = $SC->format_user_name($holder);
+
+ if (my $card = $holder->card) { # null-able
+ $details->{hold_patron_barcode} = $card->barcode;
+ }
+
+ if (ref $hold->pickup_lib) {
+ $pickup_lib_id = $hold->pickup_lib->id;
+ $pickup_lib_sn = $hold->pickup_lib->shortname;
+
+ } else {
+ $pickup_lib_id = $hold->pickup_lib;
+ $pickup_lib_sn = $SC->org_sn_from_id($session, $pickup_lib_id);
+ }
+
+ $details->{alert} = 1;
+ $details->{destination_loc} = $pickup_lib_sn;
+ $details->{alert_type} =
+ ($pickup_lib_id == $session->editor->requestor->ws_ou) ? '01' : '02';
+}
+
+1;
--- /dev/null
+package OpenILS::Application::SIP2::Checkout;
+use strict; use warnings;
+use DateTime;
+use DateTime::Format::ISO8601;
+use OpenILS::Utils::DateTime qw/:datetime/;
+use OpenSRF::Utils::Logger q/$logger/;
+use OpenILS::Application::AppUtils;
+use OpenILS::Application::SIP2::Common;
+my $U = 'OpenILS::Application::AppUtils';
+my $SC = 'OpenILS::Application::SIP2::Common';
+
+
+# Returns the 'circ' object on success, undef on error.
+sub checkout {
+ my ($class, $session, %params) = @_;
+
+ my $circ_details = {};
+ my $override = 0;
+
+ for (0, 1) { # 2 checkout requests max
+
+ $override =
+ perform_checkout($session, $circ_details, $override, %params);
+
+ last unless $override;
+ }
+
+ return $circ_details;
+}
+
+sub renew_all {
+ my ($class, $session, $patron_details, %params) = @_;
+
+ my $circ_details = {};
+
+ my @circ_ids = (
+ @{$patron_details->{items_out_ids}},
+ @{$patron_details->{items_overdue_ids}}
+ );
+
+ my @renewed;
+ my @unrenewed;
+ for my $circ_id (@circ_ids) {
+
+ my $circ = $session->editor->retrieve_action_circulation([
+ $circ_id, {flesh => 1, flesh_fields => {circ => ['target_copy']}}]);
+
+ my $item_barcode = $circ->target_copy->barcode;
+
+ my $detail = $class->checkout($session,
+ item_barcode => $item_barcode,
+ fee_ack => $params{fee_ack},
+ is_renew => 1
+ );
+
+ if ($detail->{ok}) {
+ push(@renewed, $item_barcode);
+ } else {
+ push(@unrenewed, $item_barcode);
+ }
+ }
+
+ $circ_details->{items_renewed} = \@renewed;
+ $circ_details->{items_unrenewed} = \@unrenewed;
+
+ return $circ_details;
+}
+
+# Returns 1 if the checkout should be performed again with override.
+# Returns 0 if there's nothing left to do (final success / error)
+# Updates $circ_details along the way.
+sub perform_checkout {
+ my ($session, $circ_details, $override, %params) = @_;
+ my $config = $session->config;
+
+ my $action = $params{is_renew} ? 'renew' : 'checkout';
+
+ my $args = {
+ copy_barcode => $params{item_barcode},
+ # During renewal, the circ API will confirm the specified
+ # patron has the specified item checked out before renewing.
+ patron_barcode => $params{patron_barcode}
+ };
+
+ my $method = $action eq 'renew' ?
+ 'open-ils.circ.renew' : 'open-ils.circ.checkout.full';
+
+ $method .= '.override' if $override;
+
+ my $resp = $U->simplereq(
+ 'open-ils.circ', $method, $session->editor->authtoken, $args);
+
+ $resp = [$resp] unless ref $resp eq 'ARRAY';
+
+ for my $event (@$resp) {
+ next unless $U->is_event($event); # this should never happen.
+ my $textcode = $event->{textcode};
+
+ if ($textcode eq 'SUCCESS' && $event->{payload}) {
+ if (my $circ = $event->{payload}->{circ}) {
+ $circ_details->{circ} = $circ;
+
+ my $due_date=
+ DateTime::Format::ISO8601->new
+ ->parse_datetime(clean_ISO8601($circ->due_date));
+
+ $circ_details->{due_date} =
+ $config->{due_date_use_sip_date_format} ?
+ $SC->sipdate($due_date) :
+ $due_date->strftime('%F %T');
+
+ return 0;
+ }
+ }
+
+ if (!$override) {
+ if ($config->{"$action.override.$textcode"}) {
+ # Event type is configured for override;
+ return 1;
+
+ } elsif ($params{fee_ack} &&
+ $textcode =~ /ITEM_(?:DEPOSIT|RENTAL)_FEE_REQUIRED/ ) {
+ # Patron acknowledged the fee. Redo with override.
+ return 1;
+ }
+ }
+
+ if ($textcode eq 'OPEN_CIRCULATION_EXISTS' ) {
+ my $msg = $session->editor
+ ->retrieve_sip_screen_message('checkout.open_circ_exists');
+
+ $circ_details->{screen_msg} =
+ $msg ? $msg->message : 'This item is already checked out';
+
+ } else {
+
+ my $msg =
+ $session->editor
+ ->retrieve_sip_screen_message('checkout.patron_not_allowed');
+
+ $circ_details->{screen_msg} = $msg ? $msg->message :
+ 'Patron is not allowed to checkout the selected item';
+ }
+ }
+
+ return 0;
+}
+
+
+1;
--- /dev/null
+package OpenILS::Application::SIP2::Common;
+use strict; use warnings;
+use OpenILS::Utils::DateTime qw/:datetime/;
+use OpenSRF::Utils::Cache;
+
+use constant SIP_DATE_FORMAT => "%Y%m%d %H%M%S";
+
+my $_cache;
+sub cache {
+ $_cache = OpenSRF::Utils::Cache->new unless $_cache;
+ return $_cache;
+}
+
+sub add_field {
+ my ($class, $message, $field, $value) = @_;
+ $value = '' unless defined $value;
+ push (@{$message->{fields}}, {$field => $value});
+}
+
+sub maybe_add_field {
+ my ($class, $message, $field, $value) = @_;
+ push (@{$message->{fields}}, {$field => $value}) if defined $value;
+}
+
+sub sipdate {
+ my ($class, $date) = @_;
+ $date ||= DateTime->now;
+ return $date->strftime(SIP_DATE_FORMAT);
+}
+
+sub sipymd {
+ my ($class, $date_str, $to_local_tz) = @_;
+ return '' unless $date_str;
+
+ my $dt = DateTime::Format::ISO8601->new
+ ->parse_datetime(clean_ISO8601($date_str));
+
+ # actor.usr.dob stores dates without time/timezone, which causes
+ # DateTime to assume the date is stored as UTC. Tell DateTime to
+ # use the local time zone, instead. Other dates will have time
+ # zones and should be parsed as-is.
+ $dt->set_time_zone('local') if $to_local_tz;
+
+ return $dt->strftime('%Y%m%d');
+}
+
+# False == 'N'
+sub sipbool {
+ my ($class, $bool) = @_;
+ return $bool ? 'Y' : 'N';
+}
+
+# False == ' '
+sub spacebool {
+ my ($class, $bool) = @_;
+ return $bool ? 'Y' : ' ';
+}
+
+sub count4 {
+ my ($class, $value) = @_;
+ return ' ' unless defined $value;
+ return sprintf("%04d", $value);
+}
+
+# Returns the value of the first occurrence of the requested SIP code.
+sub get_field_value {
+ my ($class, $message, $code) = @_;
+ for my $field (@{$message->{fields}}) {
+ while (my ($c, $v) = each(%$field)) { # one pair per field
+ return $v if $c eq $code;
+ }
+ }
+
+ return undef;
+}
+
+my %org_sn_cache; # shortname => org
+my %org_id_cache; # id => org
+sub org_id_from_sn {
+ my ($class, $session, $org_sn) = @_;
+
+ return undef unless $org_sn;
+
+ my $org = $org_sn_cache{$org_sn} ||
+ $session->editor->search_actor_org_unit({shortname => $org_sn})->[0];
+
+ return undef unless $org;
+
+ $org_sn_cache{$org_sn} = $org;
+ $org_id_cache{$org->id} = $org;
+
+ return $org->id;
+}
+
+sub org_sn_from_id {
+ my ($class, $session, $org_id) = @_;
+
+ return undef unless $org_id;
+
+ my $org = $org_id_cache{$org_id} ||
+ $session->editor->retrieve_actor_org_unit($org_id);
+
+ return undef unless $org;
+
+ $org_sn_cache{$org->shortname} = $org;
+ $org_id_cache{$org_id} = $org;
+
+ return $org->shortname;
+}
+
+# Determines which class of data the SIP client wants detailed
+# information on in the patron info request.
+sub patron_summary_list_items {
+ my ($class, $summary) = @_;
+
+ my $idx = index($summary, 'Y');
+
+ return 'hold_items' if $idx == 0;
+ return 'overdue_items' if $idx == 1;
+ return 'charged_items' if $idx == 2;
+ return 'fine_items' if $idx == 3;
+ return 'recall_items' if $idx == 4;
+ return 'unavailable_holds' if $idx == 5;
+ return '';
+}
+
+sub format_user_name {
+ my ($class, $user) = @_;
+ return sprintf('%s%s%s',
+ $user->first_given_name ? $user->first_given_name : '',
+ $user->second_given_name ? ' ' . $user->second_given_name : '',
+ $user->family_name ? ' ' . $user->family_name : ''
+ );
+}
+
+
+1;
--- /dev/null
+package OpenILS::Application::SIP2::Hold;
+use strict; use warnings;
+use OpenSRF::Utils::Logger q/$logger/;
+use OpenILS::Application::AppUtils;
+use OpenILS::Application::SIP2::Common;
+my $U = 'OpenILS::Application::AppUtils';
+my $SC = 'OpenILS::Application::SIP2::Common';
+
+
+sub cancel {
+ my ($class, $session, $hold) = @_;
+
+ my $details = {ok => 0};
+
+ my $resp = $U->simplereq(
+ 'open-ils.circ',
+ 'open-ils.circ.hold.cancel',
+ $session->editor->authtoken, $hold->id, 7 # cancel via SIP
+ );
+
+ return $details unless $resp && !$U->event_code($resp);
+
+ $details->{ok} = 1;
+
+ return $details;
+}
+
+# Given a "representative" copy, finds a matching hold
+sub hold_from_copy {
+ my ($class, $session, $patron_details, $item_details) = @_;
+ my $e = $session->editor;
+ my $hold;
+
+ my $copy = $item_details->{item};
+
+ my $run_hold_query = sub {
+ my %filter = @_;
+ return $e->search_action_hold_request([
+ { usr => $patron_details->{patron}->id,
+ cancel_time => undef,
+ fulfillment_time => undef,
+ %filter
+ }, {
+ flesh => 2,
+ flesh_fields => {
+ ahr => ['current_copy'],
+ acp => ['call_number']
+ },
+ order_by => {ahr => 'request_time DESC'},
+ limit => 1
+ }
+ ])->[0];
+ };
+
+ # first see if there is a match on current_copy
+ return $hold if $hold =
+ $run_hold_query->(current_copy => $copy->id);
+
+ # next, assume bib-level holds are the most common
+ return $hold if $hold = $run_hold_query->(
+ target => $copy->call_number->record->id, hold_type => 'T');
+
+ # next try metarecord holds
+ my $map = $e->search_metabib_metarecord_source_map(
+ {source => $copy->call_number->record->id})->[0];
+
+ return $hold if $hold = $run_hold_query->(
+ target => $map->metarecord, hold_type => 'M');
+
+ # volume holds
+ return $hold if $hold = $run_hold_query->(
+ target => $copy->call_number->id, hold_type => 'V');
+
+ # copy holds
+ return $run_hold_query->(
+ target => $copy->id, hold_type => ['C', 'F', 'R']);
+}
+
--- /dev/null
+package OpenILS::Application::SIP2::Item;
+use strict; use warnings;
+use DateTime;
+use DateTime::Format::ISO8601;
+use OpenSRF::System;
+use OpenILS::Utils::CStoreEditor q/:funcs/;
+use OpenSRF::Utils::Logger q/$logger/;
+use OpenILS::Application::AppUtils;
+use OpenILS::Utils::DateTime qw/:datetime/;
+use OpenILS::Const qw/:const/;
+use OpenILS::Application::SIP2::Common;
+my $U = 'OpenILS::Application::AppUtils';
+my $SC = 'OpenILS::Application::SIP2::Common';
+
+sub get_item_details {
+ my ($class, $session, %params) = @_;
+
+ my $config = $session->config;
+ my $barcode = $params{barcode};
+ my $e = $session->editor;
+
+ my $item = $e->search_asset_copy([{
+ barcode => $barcode,
+ deleted => 'f'
+ }, {
+ flesh => 3,
+ flesh_fields => {
+ acp => [qw/circ_lib call_number
+ status stat_cat_entry_copy_maps circ_modifier/],
+ acn => [qw/owning_lib record/],
+ bre => [qw/flat_display_entries/],
+ ascecm => [qw/stat_cat stat_cat_entry/],
+ }
+ }])->[0];
+
+ return undef unless $item;
+
+ my $details = {
+ item => $item,
+ security_marker => '02', # matches SIP/Item.pm
+ owning_loc => $item->call_number->owning_lib->shortname,
+ current_loc => $item->circ_lib->shortname,
+ permanent_loc => $item->circ_lib->shortname,
+ destination_loc => $item->circ_lib->shortname # maybe replaced below
+ };
+
+ $details->{circ} = $e->search_action_circulation([{
+ target_copy => $item->id,
+ checkin_time => undef,
+ '-or' => [
+ {stop_fines => undef},
+ {stop_fines => [qw/MAXFINES LONGOVERDUE/]},
+ ]
+ }, {
+ flesh => 2,
+ flesh_fields => {circ => ['usr'], au => ['card']}
+ }])->[0];
+
+ if ($details->{circ}) {
+
+ my $due_date = DateTime::Format::ISO8601->new->
+ parse_datetime(clean_ISO8601($details->{circ}->due_date));
+
+ $details->{due_date} =
+ $config->{due_date_use_sip_date_format} ?
+ $SC->sipdate($due_date) :
+ $due_date->strftime('%F %T');
+ }
+
+ if ($item->status->id == OILS_COPY_STATUS_IN_TRANSIT) {
+ $details->{transit} = $e->search_action_transit_copy([{
+ target_copy => $item->id,
+ dest_recv_time => undef,
+ cancel_time => undef
+ },{
+ flesh => 1,
+ flesh_fields => {atc => ['dest']}
+ }])->[0];
+
+ $details->{destination_loc} = $details->{transit}->dest->shortname;
+ }
+
+ if ($item->status->id == OILS_COPY_STATUS_ON_HOLDS_SHELF || (
+ $details->{transit} &&
+ $details->{transit}->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF)) {
+
+ $details->{hold} = $e->search_action_hold_request([{
+ current_copy => $item->id,
+ capture_time => {'!=' => undef},
+ cancel_time => undef,
+ fulfillment_time => undef
+ }, {
+ limit => 1,
+ flesh => 1,
+ flesh_fields => {ahr => ['pickup_lib']}
+ }])->[0];
+ }
+
+ if (my $hold = $details->{hold}) {
+ my $pickup_date = $hold->shelf_expire_time;
+ $details->{hold_pickup_date} =
+ $pickup_date ? $SC->sipdate($pickup_date) : undef;
+
+ my $card = $e->search_actor_card({usr => $hold->usr})->[0];
+ $details->{hold_patron_barcode} = $card->barcode if $card;
+ $details->{destination_loc} = $hold->pickup_lib->shortname;
+ }
+
+ my ($title_entry) = grep {$_->name eq 'title'}
+ @{$item->call_number->record->flat_display_entries};
+
+ $details->{title} = $title_entry ? $title_entry->value : '';
+
+ # Same as ../SIP*
+ $details->{hold_queue_length} = $details->{hold} ? 1 : 0;
+
+ $details->{circ_status} = circulation_status($item->status->id);
+
+ $details->{fee_type} =
+ ($item->deposit_amount > 0.0 && $item->deposit eq 'f') ?
+ '06' : '01';
+
+ my $cmod = $item->circ_modifier;
+ $details->{magnetic_media} = $cmod && $cmod->magnetic_media eq 't';
+ $details->{media_type} = $cmod ? $cmod->sip2_media_type : '001';
+
+ return $details;
+}
+
+# Maps item status to SIP circulation status constants.
+sub circulation_status {
+ my $stat = shift;
+
+ return '02' if $stat == OILS_COPY_STATUS_ON_ORDER;
+ return '03' if $stat == OILS_COPY_STATUS_AVAILABLE;
+ return '04' if $stat == OILS_COPY_STATUS_CHECKED_OUT;
+ return '06' if $stat == OILS_COPY_STATUS_IN_PROCESS;
+ return '08' if $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF;
+ return '09' if $stat == OILS_COPY_STATUS_RESHELVING;
+ return '10' if $stat == OILS_COPY_STATUS_IN_TRANSIT;
+ return '12' if (
+ $stat == OILS_COPY_STATUS_LOST ||
+ $stat == OILS_COPY_STATUS_LOST_AND_PAID
+ );
+ return '13' if $stat == OILS_COPY_STATUS_MISSING;
+
+ return '01';
+}
+
+1
+
--- /dev/null
+package OpenILS::Application::SIP2::Patron;
+use strict; use warnings;
+use DateTime;
+use DateTime::Format::ISO8601;
+use OpenSRF::System;
+use OpenILS::Utils::CStoreEditor q/:funcs/;
+use OpenSRF::Utils::Logger q/$logger/;
+use OpenILS::Const qw/:const/;
+use OpenILS::Application::AppUtils;
+use OpenILS::Utils::DateTime qw/:datetime/;
+use OpenILS::Application::SIP2::Common;
+my $U = 'OpenILS::Application::AppUtils';
+my $SC = 'OpenILS::Application::SIP2::Common';
+
+sub get_patron_details {
+ my ($class, $session, %params) = @_;
+
+ my $barcode = $params{barcode};
+ my $password = $params{password};
+
+ my $e = $session->editor;
+ my $details = {};
+
+ my $card = $e->search_actor_card([{
+ barcode => $barcode
+ }, {
+ flesh => 3,
+ flesh_fields => {
+ ac => [qw/usr/],
+ au => [qw/
+ billing_address
+ mailing_address
+ profile
+ stat_cat_entries
+ /],
+ actscecm => [qw/stat_cat/]
+ }
+ }])->[0];
+
+ return undef unless $card;
+
+ my $patron = $details->{patron} = $card->usr;
+ $patron->card($card);
+
+ # We only attempt to verify the password if one is provided.
+ return undef if defined $password &&
+ !$U->verify_migrated_user_password($e, $patron->id, $password);
+
+ set_patron_privileges($session, $details);
+
+ my $summary = $e->retrieve_money_open_user_summary($patron->id);
+ $details->{balance_owed} = ($summary) ? $summary->balance_owed : 0;
+
+ set_patron_summary_items($session, $details, %params);
+ set_patron_summary_list_items($session, $details, %params);
+ log_activity($session, $patron);
+
+ return $details;
+}
+
+sub log_activity {
+ my ($session, $patron) = @_;
+
+ my $ewho = $session->sip_account->activity_who
+ || $session->config->{default_activity_who};
+ $U->log_user_activity($patron->id, $ewho, 'verify');
+}
+
+
+# Sets:
+# holds_count
+# overdue_count
+# out_count
+# fine_count
+# recall_count
+# unavail_holds_count
+sub set_patron_summary_items {
+ my ($session, $details, %params) = @_;
+
+ my $patron = $details->{patron};
+ my $e = $session->editor;
+
+ $details->{recall_count} = 0; # not supported
+
+ $details->{hold_ids} = get_hold_ids($session, $patron);
+ $details->{holds_count} = scalar(@{$details->{hold_ids}});
+
+ $details->{unavailable_hold_ids} = get_hold_ids($session, $patron, 1);
+ $details->{unavail_holds_count} = scalar(@{$details->{unavailable_hold_ids}});
+
+ $details->{overdue_count} = 0;
+ $details->{out_count} = 0;
+
+ my $circ_summary = $e->retrieve_action_open_circ_list($patron->id);
+ if ($circ_summary) { # undef if no circs for user
+ my $overdue_ids = [ grep {$_ > 0} split(',', $circ_summary->overdue) ];
+ my $out_ids = [ grep {$_ > 0} split(',', $circ_summary->out) ];
+ $details->{overdue_count} = scalar(@$overdue_ids);
+ $details->{out_count} = scalar(@$out_ids) + scalar(@$overdue_ids);
+ $details->{items_overdue_ids} = $overdue_ids;
+ $details->{items_out_ids} = $out_ids;
+ }
+
+ my $xacts = $U->simplereq(
+ 'open-ils.actor',
+ 'open-ils.actor.user.transactions.history.have_balance',
+ $session->editor->authtoken,
+ $patron->id
+ );
+
+ $details->{fine_count} = scalar(@$xacts);
+}
+
+sub get_hold_ids {
+ my ($session, $patron, $unavail, $offset, $limit) = @_;
+
+ my $e = $session->editor;
+
+ my $holds_where = {
+ usr => $patron->id,
+ fulfillment_time => undef,
+ cancel_time => undef
+ };
+
+ if ($unavail) {
+ $holds_where->{'-or'} = [
+ {current_shelf_lib => undef},
+ {current_shelf_lib => {'!=' => {'+ahr' => 'pickup_lib'}}}
+ ];
+
+ } else {
+
+ $holds_where->{current_shelf_lib} = {'=' => {'+ahr' => 'pickup_lib'}}
+ if $session->config->{msg64_hold_items_available};
+ }
+
+ my $query = {
+ select => {ahr => ['id']},
+ from => 'ahr',
+ where => {'+ahr' => $holds_where}
+ };
+
+ $query->{offset} = $offset if $offset;
+ $query->{limit} = $limit if $limit;
+
+ my $id_hashes = $e->json_query($query);
+
+ return [map {$_->{id}} @$id_hashes];
+}
+
+sub set_patron_summary_list_items {
+ my ($session, $details, %params) = @_;
+ my $e = $session->editor;
+
+ my $list_items = $params{summary_list_items};
+
+ return unless $list_items;
+
+ # Start and end are 1-based. Translate to zero-based for internal use.
+ my $offset = $params{summary_start_item} ? $params{summary_start_item} - 1 : 0;
+ my $end = $params{summary_end_item} ? $params{summary_end_item} - 1 : 10;
+ my $limit = $end - $offset;
+
+ add_hold_items($session, $details, $offset, $limit)
+ if $list_items eq 'hold_items';
+
+ add_hold_items($session, $details, $offset, $limit, 1)
+ if $list_items eq 'unavailable_holds';
+
+ add_items_out($session, $details, $offset, $limit)
+ if $list_items eq 'charged_items';
+
+ add_items_out($session, $details, $offset, $limit)
+ if $list_items eq 'charged_items';
+
+ add_fine_items($session, $details, $offset, $limit)
+ if $list_items eq 'fine_items';
+
+}
+
+sub add_hold_items {
+ my ($session, $details, $offset, $limit, $unavailable) = @_;
+
+ my $patron = $details->{patron};
+ my $format = $session->config->{msg64_hold_datatype} || '';
+ my $hold_ids = $unavailable ?
+ $details->{unavailable_hold_ids} : $details->{hold_ids};
+
+ my @hold_items;
+ for my $hold_id (@$hold_ids) {
+ my $hold = $session->editor->retrieve_action_hold_request($hold_id);
+
+ if ($format eq 'barcode') {
+ my $copy = find_copy_for_hold($session, $hold);
+ push(@hold_items, $copy->barcode) if $copy;
+ } else {
+ my $title = find_title_for_hold($session, $hold);
+ push(@hold_items, $title) if $title;
+ }
+ }
+
+ $details->{hold_items} = \@hold_items;
+}
+
+sub add_items_out {
+ my ($session, $details, $offset, $limit) = @_;
+ my $patron = $details->{patron};
+
+ my @circ_ids = (@{$details->{items_out_ids}}, @{$details->{items_overdue_ids}});
+
+ @circ_ids = grep { $_ } @circ_ids[$offset .. ($offset + $limit - 1)];
+
+ $details->{items_out} = [];
+ for my $circ_id (@circ_ids) {
+ my $value = circ_id_to_value($session, $circ_id);
+ push(@{$details->{items_out}}, $value);
+ }
+}
+
+sub add_overdue_items {
+ my ($session, $details, $offset, $limit) = @_;
+ my $patron = $details->{patron};
+
+ my @circ_ids = @{$details->{items_overdue_ids}};
+
+ @circ_ids = grep { $_ } @circ_ids[$offset .. ($offset + $limit - 1)];
+
+ $details->{overdue_items} = [];
+ for my $circ_id (@circ_ids) {
+ my $value = circ_id_to_value($session, $circ_id);
+ push(@{$details->{items_out}}, $value);
+ }
+}
+
+sub circ_id_to_value {
+ my ($session, $circ_id) = @_;
+
+ my $value = '';
+ my $format = $session->config->{settings}->{msg64_summary_datatype} || '';
+
+ if ($format eq 'barcode') {
+ my $circ = $session->editor->retrieve_action_circulation([
+ $circ_id, {
+ flesh => 1,
+ flesh_fields => {circ => ['target_copy']}
+ }]);
+
+ $value = $circ->target_copy->barcode;
+
+ } else { # title
+
+ my $circ = $session->editor->retrieve_action_circulation([
+ $circ_id, {
+ flesh => 4,
+ flesh_fields => {
+ circ => ['target_copy'],
+ acp => ['call_number'],
+ acn => ['record'],
+ bre => ['simple_record']
+ }
+ }]);
+
+ if ($circ->target_copy->call_number == -1) {
+ $value = $circ->target_copy->dummy_title;
+ } else {
+ $value =
+ $circ->target_copy->call_number->record->simple_record->title;
+ }
+ }
+
+ return $value;
+}
+
+# Hold -> reporter.hold_request_record -> display field for title.
+sub find_title_for_hold {
+ my ($session, $hold) = @_;
+ my $e = $session->editor;
+
+ my $bib_link = $e->retrieve_reporter_hold_request_record($hold->id);
+
+ my $title_field = $e->search_metabib_flat_display_entry({
+ source => $bib_link->bib_record, name => 'title'})->[0];
+
+ return $title_field ? $title_field->value : '';
+}
+
+# Finds a representative copy for the given hold. If no copy exists at
+# all, undef is returned. The only limit placed on what constitutes a
+# "representative" copy is that it cannot be deleted. Otherwise, any
+# copy that allows us to find the hold later is good enough.
+sub find_copy_for_hold {
+ my ($session, $hold) = @_;
+ my $e = $session->editor;
+
+ return $e->retrieve_asset_copy($hold->current_copy)
+ if $hold->current_copy;
+
+ return $e->retrieve_asset_copy($hold->target)
+ if $hold->hold_type =~ /C|R|F/;
+
+ return $e->search_asset_copy([
+ {call_number => $hold->target, deleted => 'f'},
+ {limit => 1}])->[0] if $hold->hold_type eq 'V';
+
+ my $bre_ids = [$hold->target];
+
+ if ($hold->hold_type eq 'M') {
+ # find all of the bibs that link to the target metarecord
+ my $maps = $e->search_metabib_metarecord_source_map(
+ {metarecord => $hold->target});
+ $bre_ids = [map {$_->record} @$maps];
+ }
+
+ my $vol_ids = $e->search_asset_call_number(
+ {record => $bre_ids, deleted => 'f'},
+ {idlist => 1}
+ );
+
+ return $e->search_asset_copy([
+ {call_number => $vol_ids, deleted => 'f'},
+ {limit => 1}
+ ])->[0];
+}
+
+
+sub set_patron_privileges {
+ my ($session, $details) = @_;
+ my $patron = $details->{patron};
+
+ my $expire = DateTime::Format::ISO8601->new
+ ->parse_datetime(clean_ISO8601($patron->expire_date));
+
+ if ($expire < DateTime->now) {
+ $logger->info("SIP2 Patron account is expired; all privileges blocked");
+ $details->{charge_denied} = 1;
+ $details->{recall_denied} = 1;
+ $details->{renew_denied} = 1;
+ $details->{holds_denied} = 1;
+ return;
+ }
+
+ # Non-expired patrons are allowed all privileges when
+ # patron_status_permit_all is true.
+ return if $session->config->{patron_status_permit_all};
+
+ my $penalties = get_patron_penalties($session, $patron);
+
+ $details->{too_many_overdue} = 1 if
+ grep {$_->{id} == OILS_PENALTY_PATRON_EXCEEDS_OVERDUE_COUNT}
+ @$penalties;
+
+ $details->{too_many_fines} = 1 if
+ grep {$_->{id} == OILS_PENALTY_PATRON_EXCEEDS_FINES}
+ @$penalties;
+
+ my $blocked = (
+ $patron->barred eq 't'
+ || $patron->active eq 'f'
+ || $patron->card->active eq 'f'
+ );
+
+ my @block_tags = map {$_->{block_list}} grep {$_->{block_list}} @$penalties;
+
+ return unless $blocked || @block_tags; # no blocks remain
+
+ $details->{holds_denied} = ($blocked || grep {$_ =~ /HOLD/} @block_tags);
+
+ # Ignore loan-related blocks?
+ return if $session->config->{patron_status_permit_loans};
+
+ $details->{charge_denied} = ($blocked || grep {$_ =~ /CIRC/} @block_tags);
+ $details->{renew_denied} = ($blocked || grep {$_ =~ /RENEW/} @block_tags);
+
+ # In evergreen, patrons cannot create Recall holds directly, but that
+ # doesn't mean they would not have said privilege if the functionality
+ # existed. Base the ability to perform recalls on whether they have
+ # checkout and holds privilege, since both would be needed for recalls.
+ $details->{recall_denied} =
+ ($details->{charge_denied} || $details->{holds_denied});
+
+}
+
+# Returns an array of penalty hashes with keys "id" and "block_list"
+sub get_patron_penalties {
+ my ($session, $patron) = @_;
+
+ return $session->editor->json_query({
+ select => {csp => ['id', 'block_list']},
+ from => {ausp => 'csp'},
+ where => {
+ '+ausp' => {
+ usr => $patron->id,
+ '-or' => [
+ {stop_date => undef},
+ {stop_date => {'>' => 'now'}}
+ ],
+ org_unit =>
+ $U->get_org_full_path($session->editor->requestor->ws_ou)
+ }
+ }
+ });
+}
+
+sub add_fine_items {
+ my ($session, $details, $offset, $limit) = @_;
+ my $patron = $details->{patron};
+ my $e = $session->editor;
+
+ my @fines;
+ my $AV_format = lc($session->config->{settings}->{av_format} || 'eg_legacy');
+
+ # Do a prescan for validity and default to eg_legacy
+ if ($AV_format ne "swyer_a" &&
+ $AV_format ne "swyer_b" &&
+ $AV_format ne "eg_legacy" &&
+ $AV_format ne "3m") {
+
+ syslog(LOG_WARNING => "SIP2 Unknown value for AV_format: $AV_format");
+ $AV_format = "eg_legacy";
+ }
+
+ my $xacts = $U->simplereq(
+ 'open-ils.actor',
+ 'open-ils.actor.user.transactions.history.have_balance',
+ $e->authtoken, $patron->id
+ );
+
+ foreach my $xact (@{$xacts}) {
+ my ($title, $author, $line, $fee_type);
+
+ if ($xact->last_billing_type eq 'Lost Materials') {
+ $fee_type = 'LOST';
+ } elsif ($xact->last_billing_type =~ /^Overdue/) {
+ $fee_type = 'FINE';
+ } else {
+ $fee_type = 'FEE';
+ }
+
+ if ($xact->xact_type eq 'circulation') {
+ my $circ = $e->retrieve_action_circulation([
+ $xact->id, {
+ flesh => 2,
+ flesh_fields => {
+ circ => ['target_copy'],
+ acp => ['call_number']
+ }
+ }
+ ]);
+
+ if ($circ->target_copy->call_number->id == -1) {
+ $title = $circ->target_copy->dummy_title;
+ $author = $circ->target_copy->dummy_author;
+
+ } else {
+
+ my $displays = $e->search_metabib_flat_display_entry({
+ source => $circ->target_copy->call_number->record,
+ name => ['title', 'author']
+ });
+
+ ($title) = map {$_->value} grep {$_->name eq 'title'} @$displays;
+ ($author) = map {$_->value} grep {$_->name eq 'author'} @$displays;
+ }
+
+ # Scrub "/" chars since they are used in some cases
+ # to delineate title/author.
+ if ($title) {
+ $title =~ s/\///g;
+ } else {
+ $title = '';
+ }
+
+ if ($author) {
+ $author =~ s/\///g;
+ } else {
+ $author = '';
+ }
+ }
+
+ if ($AV_format eq "eg_legacy") {
+
+ $line = $xact->balance_owed . " " . $xact->last_billing_type . " ";
+
+ if ($xact->xact_type eq 'circulation') {
+ $line .= "$title / $author";
+ } else {
+ $line .= $xact->last_billing_type;
+ }
+
+ } elsif ($AV_format eq "3m" or $AV_format eq "swyer_a") {
+
+ $line = $xact->id . ' $' . $xact->balance_owed . " \"$fee_type\" ";
+
+ if ($xact->xact_type eq 'circulation') {
+ $line .= "$title";
+ } else {
+ $line .= $xact->last_billing_type;
+ }
+
+ } elsif ($AV_format eq "swyer_b") {
+
+ $line = "Charge-Number: " . $xact->id;
+ $line .= ", Amount-Due: " . $xact->balance_owed;
+ $line .= ", Fine-Type: $fee_type";
+
+ if ($xact->xact_type eq 'circulation') {
+ $line .= ", Title: $title";
+ } else {
+ $line .= ", Title: " . $xact->last_billing_type;
+ }
+ }
+
+ push @fines, $line;
+ }
+
+ $details->{fine_items} = \@fines;
+}
+
+
+1;
--- /dev/null
+package OpenILS::Application::SIP2::Payment;
+use strict; use warnings;
+use DateTime;
+use DateTime::Format::ISO8601;
+use OpenSRF::System;
+use OpenILS::Utils::CStoreEditor q/:funcs/;
+use OpenSRF::Utils::Logger q/$logger/;
+use OpenILS::Application::AppUtils;
+use OpenILS::Utils::DateTime qw/:datetime/;
+use OpenILS::Const qw/:const/;
+use OpenILS::Application::SIP2::Common;
+use OpenILS::Application::SIP2::Session;
+my $U = 'OpenILS::Application::AppUtils';
+my $SC = 'OpenILS::Application::SIP2::Common';
+
+
+sub apply_payment {
+ my ($class, $session, %params) = @_;
+
+ my $details = {ok => 0};
+
+ my $card = $session->editor->search_actor_card([
+ {barcode => $params{patron_barcode}},
+ {flesh => 1, flesh_fields => {ac => [qw/usr/]}}
+ ])->[0];
+
+ return $details unless $card;
+
+ my $user = $card->usr;
+
+ if ($params{fee_id}) {
+ pay_one_transaction($session, $details, $user, %params);
+
+ } else {
+ # No transaction was specified, pay whatever we can.
+ pay_multi_transactions($session, $details, $user, %params);
+ }
+
+ return $details;
+}
+
+sub pay_one_transaction {
+ my ($session, $details, $user, %params) = @_;
+
+ my $fee_id = $params{fee_id}; # action.billable_xact.id
+
+ my $xact =
+ $session->editor->retrieve_money_billable_transaction_summary($fee_id);
+
+ return unless $xact && $xact->usr == $user->id;
+
+ my $pay_amount = $params{pay_amount};
+
+ return unless $pay_amount > 0;
+
+ if ($pay_amount > $xact->balance_owed) {
+ my $msg = $session->editor
+ ->retrieve_sip_screen_message('payment.overpayment_not_allowed');
+
+ $details->{screen_msg} = $msg ? $msg->message : 'Overpayment not allowed';
+ return;
+ }
+
+ my $payments = [[$xact->id, $pay_amount]];
+
+ send_payments($session, $details, $user, $payments, %params);
+}
+
+sub pay_multi_transactions {
+ my ($session, $details, $user, %params) = @_;
+ my $payments = [];
+
+ # See if we can find some find some transactions to pay.
+ my $xacts = $U->simplereq('open-ils.actor',
+ 'open-ils.actor.user.transactions.history.have_balance',
+ $session->editor->authtoken, $user->id);
+
+ if (!$xacts || !@$xacts) { # nothing to pay
+ my $msg = $session->editor->
+ retrieve_sip_screen_message('payment.transaction_not_found');
+
+ $details->{screen_msg} = $msg ? $msg->message : 'Bill not found';
+ return;
+ }
+
+ my $pay_amount = $params{pay_amount};
+ my $amount_remaining = $pay_amount;
+
+ for my $xact (@$xacts) {
+ next if $xact->balance_owed <= 0;
+
+ my $payment;
+ my $xact_id = $xact->id;
+ my $balance_owed = $xact->balance_owed;
+
+ if ($balance_owed >= $amount_remaining) {
+
+ # We owe as much as or more than we have money left,
+ # so pay what we have left.
+ $payment = $amount_remaining;
+ $amount_remaining = 0;
+
+ } else {
+
+ # This bill is for less than the amount we have
+ # left, so pay the full bill amount.
+ $payment = $balance_owed;
+ $amount_remaining = $U->fpdiff($amount_remaining, $balance_owed);
+ }
+
+ push(@$payments, [$xact->id, $payment]);
+
+ $amount_remaining = sprintf("%.2f", $amount_remaining);
+ $balance_owed = sprintf("%.2f", $balance_owed);
+
+ $logger->info("SIP paid $payment on $xact_id with a ".
+ "balance of $balance_owed and $amount_remaining remaining");
+
+ # Leave if we ran out of money.
+ last if $amount_remaining == 0;
+ }
+
+ if ($amount_remaining > 0) {
+ my $msg = $session->editor
+ ->retrieve_sip_screen_message('payment.overpayment_not_allowed');
+
+ $details->{screen_msg} = $msg ? $msg->message : 'Overpayment not allowed';
+ return;
+ }
+
+ send_payments($session, $details, $user, $payments, %params);
+}
+
+# Takes array ref of array ref of [xact_id, payment_amount] to pay in batch.
+sub send_payments {
+ my ($session, $details, $user, $payments, %params) = @_;
+
+ my $pay_type = $params{pay_type};
+ my $register_login = $params{register_login};
+
+ if ($register_login) {
+ $logger->debug("SIP register login sent as '$register_login'");
+
+ if ($register_login =~ /\\.+/) { # Windows domain login DOMAIN\user
+ my @parts = split(/\\/, $register_login);
+ $register_login = $parts[1];
+ }
+ }
+
+ my $args = {
+ userid => $user->id,
+ note => $register_login ?
+ "Via SIP2: Register login '$register_login'" : "Via SIP2",
+ payments => $payments,
+ payment_type => 'cash_payment'
+ };
+
+ if ($pay_type eq '01' || $pay_type eq '02') {
+ # '01' is "VISA"
+ # '02' is "credit card"
+
+ $args->{payment_type} = 'credit_card_payment';
+ $args->{cc_args} = {
+ approval_code =>
+ $params{terminal_xact} || 'Not provided by SIP client'
+ };
+
+ } elsif ($pay_type eq '05') {
+
+ $args->{payment_type} = 'check_payment';
+ $args->{check_number} =
+ $params{check_number} || 'Not Provided by SIP Client';
+ }
+
+ my $resp = $U->simplereq(
+ 'open-ils.circ', 'open-ils.circ.money.payment',
+ $session->editor->authtoken, $args, $user->last_xact_id);
+
+ if ($U->event_code($resp)) {
+ $details->{screen_msg} = $resp->{descr} || $resp->{textcode};
+ } else {
+ $details->{ok} = 1;
+ }
+}
+
+
+
+
+
--- /dev/null
+package OpenILS::Application::SIPSession;
+use strict; use warnings;
+use JSON::XS;
+use OpenSRF::Utils::Logger q/$logger/;
+use OpenILS::Application::AppUtils;
+use OpenILS::Utils::CStoreEditor q/:funcs/;
+use OpenILS::Utils::Fieldmapper;
+use OpenILS::Application::SIP2::Common;
+my $json = JSON::XS->new;
+my $U = 'OpenILS::Application::AppUtils';
+my $SC = 'OpenILS::Application::SIP2::Common';
+$json->ascii(1);
+$json->allow_nonref(1);
+
+# Supported Messages (BX)
+# Currently hard-coded, since it's based on availabilty of functionality
+# in the code, but it could be moved into the database to limit access for
+# specific setting groups.
+use constant INSTITUTION_SUPPORTS => [
+ 'Y', # patron status request,
+ 'Y', # checkout,
+ 'Y', # checkin,
+ 'N', # block patron,
+ 'Y', # acs status,
+ 'N', # request sc/acs resend,
+ 'Y', # login,
+ 'Y', # patron information,
+ 'N', # end patron session,
+ 'Y', # fee paid,
+ 'Y', # item information,
+ 'N', # item status update,
+ 'N', # patron enable,
+ 'N', # hold,
+ 'Y', # renew,
+ 'N', # renew all,
+];
+
+sub new {
+ my ($class, %args) = @_;
+ return bless(\%args, $class);
+}
+
+sub supports {
+ return INSTITUTION_SUPPORTS;
+}
+
+sub config {
+ my $self = shift;
+ return $self->{config} if $self->{config};
+
+ my $group = $self->editor->retrieve_sip_setting_group([
+ $self->sip_account->setting_group,
+ {flesh => 1, flesh_fields => {sipsetg => ['settings']}}
+ ]);
+
+ my $config = {
+ institution => $group->institution,
+ supports => INSTITUTION_SUPPORTS
+ };
+
+ # Decode and hashify settings for easy access
+ $config->{settings} =
+ {map {$_->name => $json->decode($_->value)} @{$group->settings}};
+
+ $logger->info("SIP settings " . $json->encode($config->{settings}));
+ return $self->{config} = $config;
+}
+
+# Retrieve an existing SIP session via SIP session token
+sub find {
+ my ($class, $seskey) = @_;
+
+ my $session = $class->new(seskey => $seskey);
+ my $e = $session->editor;
+
+ my $cache_ses = $SC->cache->get_cache("sip2_$seskey");
+
+ if ($cache_ses) {
+ $session->{sip_account} = $cache_ses->{sip_account};
+ $e->authtoken($cache_ses->{ils_token});
+ return $session if $session->set_ils_account;
+ }
+
+ # Nothing in the cache, check the DB.
+
+ my $ses = $e->retrieve_sip_session([
+ $seskey, {flesh => 1, flesh_fields => {sipses => ['account']}}]);
+
+ if ($ses) {
+ $session->{sip_account} = $ses->account;
+ $e->authtoken($ses->ils_token);
+ return $session if $session->set_ils_account($ses);
+ }
+
+ $logger->warn("SIP2: No session found for key $seskey");
+ return undef;
+}
+
+# The editor contains the authtoken and ILS user account (requestor).
+sub editor {
+ my $self = shift;
+ $self->{editor} = new_editor() unless $self->{editor};
+ return $self->{editor};
+}
+
+sub seskey {
+ my $self = shift;
+ return $self->{seskey};
+}
+
+# SIP account
+sub sip_account {
+ my $self = shift;
+ return $self->{sip_account};
+}
+
+# Logs in to Evergreen and stores the auth token/login with the SIP
+# account data.
+# Returns true on success, false on failure to authenticate.
+sub set_ils_account {
+ my $self = shift;
+ my $ses = shift;
+ my $e = $self->editor;
+ my $account = $self->sip_account;
+
+ return 1 if $e->authtoken && $e->checkauth;
+
+ my $args = {
+ user_id => $account->usr,
+ login_type => 'staff'
+ };
+
+ $args->{workstation} = $account->workstation->name
+ if $account->workstation;
+
+ my $auth = $U->simplereq(
+ 'open-ils.auth_internal',
+ 'open-ils.auth_internal.session.create', $args);
+
+ if ($auth->{textcode} ne 'SUCCESS') {
+ $logger->warn(
+ "SIP2 failed to create an internal login session for ILS user: ".
+ $account->usr);
+ return 0;
+ }
+
+ my $seskey = $self->seskey;
+ my $ils_token = $auth->{payload}->{authtoken};
+ $e->authtoken($ils_token);
+
+ my $cache_ses = {
+ sip_account => $account,
+ ils_token => $ils_token
+ };
+
+ $SC->cache->put_cache("sip2_$seskey", $cache_ses);
+
+ # transient account sessions are not tracked in the database
+ return 1 if $U->is_true($account->transient);
+
+ $e->xact_begin;
+
+ if ($ses) {
+ # ILS token expired on an existing SIP session.
+ # Update the session to use the new token.
+
+ $ses->ils_token($ils_token);
+ unless ($e->udpate_sip_session($ses)) {
+ $e->rollback;
+ return 0;
+ }
+
+ } else {
+ # New session
+
+ my $ses = Fieldmapper::sip::session->new;
+ $ses->key($seskey);
+ $ses->ils_token($ils_token);
+ $ses->account($account->id);
+
+ unless ($e->create_sip_session($ses)) {
+ $e->rollback;
+ return 0;
+ }
+ }
+
+ $e->xact_commit;
+
+ return 1;
+}
+
+1;
--- /dev/null
+# ---------------------------------------------------------------
+# Copyright (C) 2020 King County Library System
+# Bill Erickson <berickxx@gmail.com>
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# ---------------------------------------------------------------
+# Code borrows heavily and sometimes copies directly from from
+# ../SIP* and SIPServer*
+# ---------------------------------------------------------------
+package OpenILS::WWW::SIP2Mediator;
+use strict; use warnings;
+use Apache2::Const -compile =>
+ qw(OK FORBIDDEN NOT_FOUND HTTP_INTERNAL_SERVER_ERROR HTTP_BAD_REQUEST);
+use Apache2::RequestRec;
+use CGI;
+use JSON::XS;
+use OpenSRF::System;
+use OpenSRF::Utils::Logger q/$logger/;
+use OpenILS::Application::AppUtils;
+my $U = 'OpenILS::Application::AppUtils';
+
+my $json = JSON::XS->new;
+$json->ascii(1);
+$json->allow_nonref(1);
+
+my $osrf_config;
+sub import {
+ $osrf_config = shift;
+}
+
+my $init_complete = 0;
+sub init {
+ return if $init_complete;
+ $init_complete = 1;
+ OpenSRF::System->bootstrap_client(config_file => $osrf_config);
+}
+
+sub handler {
+ my $r = shift;
+ my $cgi = CGI->new;
+ my ($message, $msg_code);
+
+ init();
+
+ my $seskey = $cgi->param('session');
+ my $msg_json = $cgi->param('message');
+
+ # sip2-mediator generates a unique key for each client session.
+ # This key is required even if the client has not yet authenticated.
+ return Apache2::Const::FORBIDDEN unless $seskey;
+
+ if ($msg_json) {
+ eval { $message = $json->decode($msg_json) };
+ if ($message) {
+ $msg_code = $message->{code};
+ } else {
+ $logger->error("SIP2: Error parsing message JSON: $@ : $msg_json");
+ }
+ }
+
+ return Apache2::Const::HTTP_BAD_REQUEST unless $msg_code;
+
+ my $response = $U->simplereq(
+ 'open-ils.sip2',
+ 'open-ils.sip2.request', $seskey, $message);
+
+ if (!$response) {
+
+ # It's OK not receive a response after an End Session message
+ return Apache2::Const::OK if $msg_code eq 'XS';
+
+ $logger->error("SIP2: API Request returned no value for: $msg_json");
+ return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
+
+ } elsif (my $textcode = $response->{textcode}) {
+
+ # SIP API returned a failure event
+ $logger->error("SIP2: API request returned $textcode: $msg_json");
+
+ return Apache2::Const::FORBIDDEN if $textcode eq 'PERM_FAILURE';
+
+ return Apache2::Const::HTTP_BAD_REQUEST;
+ }
+
+ $r->content_type('application/json');
+ $r->print($json->encode($response));
+
+ return Apache2::Const::OK;
+}
+
+1;
--- /dev/null
+BEGIN;
+
+DROP SCHEMA IF EXISTS sip CASCADE;
+
+CREATE SCHEMA sip;
+
+-- Collections of settings that can be linked to one or more SIP accounts.
+CREATE TABLE sip.setting_group (
+ id SERIAL PRIMARY KEY,
+ label TEXT UNIQUE NOT NULL,
+ institution TEXT NOT NULL -- Duplicates OK
+);
+
+-- Key/value setting pairs
+CREATE TABLE sip.setting (
+ id SERIAL PRIMARY KEY,
+ setting_group INTEGER NOT NULL REFERENCES sip.setting_group (id)
+ ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+ name TEXT NOT NULL,
+ description TEXT NOT NULL,
+ value JSON NOT NULL,
+ CONSTRAINT name_once_per_inst UNIQUE (setting_group, name)
+);
+
+CREATE TABLE sip.account (
+ id SERIAL PRIMARY KEY,
+ enabled BOOLEAN NOT NULL DEFAULT TRUE,
+ setting_group INTEGER NOT NULL REFERENCES sip.setting_group (id)
+ DEFERRABLE INITIALLY DEFERRED,
+ sip_username TEXT NOT NULL,
+ usr BIGINT NOT NULL REFERENCES actor.usr(id)
+ DEFERRABLE INITIALLY DEFERRED,
+ workstation INTEGER REFERENCES actor.workstation(id),
+ -- sessions for transient accounts are not tracked in sip.session
+ transient BOOLEAN NOT NULL DEFAULT FALSE,
+ activity_who TEXT -- config.usr_activity_type.ewho
+);
+
+CREATE TABLE sip.session (
+ key TEXT PRIMARY KEY,
+ ils_token TEXT NOT NULL UNIQUE,
+ account INTEGER NOT NULL REFERENCES sip.account(id)
+ ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+ create_time TIMESTAMPTZ NOT NULL DEFAULT NOW()
+);
+
+CREATE TABLE sip.screen_message (
+ key TEXT PRIMARY KEY,
+ message TEXT NOT NULL
+);
+
+COMMIT;
+
'cwst', 'label'
)
);
+
+INSERT INTO actor.passwd_type (code, name, login, crypt_algo, iter_count)
+ VALUES ('sip2', 'SIP2 Client Password', FALSE, 'bf', 5);
+
+-- ID 1 is magic.
+INSERT INTO sip.setting_group (id, label, institution)
+ VALUES (1, 'Default Settings', 'example');
+
+-- carve space for other canned setting groups
+SELECT SETVAL('sip.setting_group_id_seq'::TEXT, 1000);
+
+-- has to be global since settings are linked to accounts and if
+-- status-before-login is used, no account information will be available.
+INSERT INTO config.global_flag (name, value, enabled, label) VALUES
+( 'sip.sc_status_before_login_institution', NULL, FALSE,
+ oils_i18n_gettext(
+ 'sip.sc_status_before_login_institution',
+ 'Activate status-before-login-support and define the institution ' ||
+ 'value which should be used in the response',
+ 'cgf', 'label')
+);
+
+INSERT INTO sip.setting (setting_group, name, value, description)
+VALUES (
+ 1, 'currency', '"USD"',
+ oils_i18n_gettext(
+ (SELECT id FROM sip.setting WHERE name = 'currency'),
+ 'Monetary amounts are reported in this currency',
+ 'sipset', 'description')
+), (
+ 1, 'av_format', '"eg_legacy"',
+ oils_i18n_gettext(
+ (SELECT id FROM sip.setting WHERE name = 'av_format'),
+ 'AV Format. Options: eg_legacy, 3m, swyer_a, swyer_b',
+ 'sipset', 'description')
+), (
+ 1, 'due_date_use_sip_date_format', 'false',
+ oils_i18n_gettext(
+ (SELECT id FROM sip.setting WHERE name = 'due_date_use_sip_date_format'),
+ 'Due date uses 18-char date format (YYYYMMDDZZZZHHMMSS). Otherwise "YYYY-MM-DD HH:MM:SS',
+ 'sipset', 'description')
+), (
+ 1, 'patron_status_permit_loans', 'false',
+ oils_i18n_gettext(
+ (SELECT id FROM sip.setting WHERE name = 'patron_status_permit_loans'),
+ 'Checkout and renewal are allowed even when penalties blocking these actions exist',
+ 'sipset', 'description')
+), (
+ 1, 'patron_status_permit_all', 'false',
+ oils_i18n_gettext(
+ (SELECT id FROM sip.setting WHERE name = 'patron_status_permit_all'),
+ 'Holds, checkouts, and renewals allowed regardless of blocking penalties',
+ 'sipset', 'description')
+), (
+ 1, 'default_activity_who', 'null',
+ oils_i18n_gettext(
+ (SELECT id FROM sip.setting WHERE name = 'default_activity_who'),
+ 'Patron holds data may be returned as either "title" or "barcode"',
+ 'sipset', 'description')
+), (
+ 1, 'msg64_summary_datatype', '"title"',
+ oils_i18n_gettext(
+ (SELECT id FROM sip.setting WHERE name = 'msg64_summary_datatype'),
+ 'Patron circulation data may be returned as either "title" or "barcode"',
+ 'sipset', 'description')
+), (
+ 1, 'msg64_hold_items_available', '"title"',
+ oils_i18n_gettext(
+ (SELECT id FROM sip.setting WHERE name = 'msg64_hold_items_available'),
+ 'Patron holds data may be returned as either "title" or "barcode"',
+ 'sipset', 'description')
+), (
+ 1, 'checkout.override.COPY_ALERT_MESSAGE', 'true',
+ oils_i18n_gettext(
+ (SELECT id FROM sip.setting WHERE name = 'checkout.override.COPY_ALERT_MESSAGE'),
+ 'Checkout override copy alert message',
+ 'sipset', 'description')
+), (
+ 1, 'checkout.override.COPY_NOT_AVAILABLE', 'true',
+ oils_i18n_gettext(
+ (SELECT id FROM sip.setting WHERE name = 'checkout.override.COPY_NOT_AVAILABLE'),
+ 'Checkout override copy not available message',
+ 'sipset', 'description')
+), (
+ 1, 'checkin.override.COPY_ALERT_MESSAGE', 'true',
+ oils_i18n_gettext(
+ (SELECT id FROM sip.setting WHERE name = 'checkin.override.COPY_ALERT_MESSAGE'),
+ 'Checkin override copy alert message',
+ 'sipset', 'description')
+), (
+ 1, 'checkin.override.COPY_BAD_STATUS', 'true',
+ oils_i18n_gettext(
+ (SELECT id FROM sip.setting WHERE name = 'checkin.override.COPY_BAD_STATUS'),
+ 'Checkin override bad copy status',
+ 'sipset', 'description')
+), (
+ 1, 'checkin.override.COPY_STATUS_MISSING', 'true',
+ oils_i18n_gettext(
+ (SELECT id FROM sip.setting WHERE name = 'checkin.override.COPY_STATUS_MISSING'),
+ 'Checkin override copy status missing',
+ 'sipset', 'description')
+), (
+ 1, 'checkin_hold_as_transit', 'false',
+ oils_i18n_gettext(
+ (SELECT id FROM sip.setting WHERE name = 'checkin_hold_as_transit'),
+ 'Checkin local holds as transits',
+ 'sipset', 'description')
+);
+
+INSERT INTO sip.screen_message (key, message) VALUES (
+ 'checkout.open_circ_exists',
+ oils_i18n_gettext(
+ 'checkout.open_circ_exists',
+ 'This item is already checked out',
+ 'sipsm', 'message')
+), (
+ 'checkout.patron_not_allowed',
+ oils_i18n_gettext(
+ 'checkout.patron_not_allowed',
+ 'Patron is not allowed to checkout the selected item',
+ 'sipsm', 'message')
+), (
+ 'payment.overpayment_not_allowed',
+ oils_i18n_gettext(
+ 'payment.overpayment_not_allowed',
+ 'Overpayment not allowed',
+ 'sipsm', 'message')
+), (
+ 'payment.transaction_not_found',
+ oils_i18n_gettext(
+ 'payment.transaction_not_found',
+ 'Bill not found',
+ 'sipsm', 'message')
+);
+
+
300.schema.staged_search.sql
400.schema.action_trigger.sql
+410.schema.sip.sql
500.view.cross-schema.sql
600.schema.oai.sql
--- /dev/null
+
+BEGIN;
+
+-- SELECT evergreen.upgrade_deps_block_check('TODO', :eg_version);
+
+DROP SCHEMA IF EXISTS sip CASCADE;
+
+CREATE SCHEMA sip;
+
+-- Collections of settings that can be linked to one or more SIP accounts.
+CREATE TABLE sip.setting_group (
+ id SERIAL PRIMARY KEY,
+ label TEXT UNIQUE NOT NULL,
+ institution TEXT NOT NULL -- Duplicates OK
+);
+
+-- Key/value setting pairs
+CREATE TABLE sip.setting (
+ id SERIAL PRIMARY KEY,
+ setting_group INTEGER NOT NULL REFERENCES sip.setting_group (id)
+ ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+ name TEXT NOT NULL,
+ description TEXT NOT NULL,
+ value JSON NOT NULL,
+ CONSTRAINT name_once_per_inst UNIQUE (setting_group, name)
+);
+
+CREATE TABLE sip.account (
+ id SERIAL PRIMARY KEY,
+ enabled BOOLEAN NOT NULL DEFAULT TRUE,
+ setting_group INTEGER NOT NULL REFERENCES sip.setting_group (id)
+ DEFERRABLE INITIALLY DEFERRED,
+ sip_username TEXT NOT NULL,
+ usr BIGINT NOT NULL REFERENCES actor.usr(id)
+ DEFERRABLE INITIALLY DEFERRED,
+ workstation INTEGER REFERENCES actor.workstation(id),
+ -- sessions for transient accounts are not tracked in sip.session
+ transient BOOLEAN NOT NULL DEFAULT FALSE,
+ activity_who TEXT -- config.usr_activity_type.ewho
+);
+
+CREATE TABLE sip.session (
+ key TEXT PRIMARY KEY,
+ ils_token TEXT NOT NULL UNIQUE,
+ account INTEGER NOT NULL REFERENCES sip.account(id)
+ ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+ create_time TIMESTAMPTZ NOT NULL DEFAULT NOW()
+);
+
+CREATE TABLE sip.screen_message (
+ key TEXT PRIMARY KEY,
+ message TEXT NOT NULL
+);
+
+-- SEED DATA
+
+INSERT INTO actor.passwd_type (code, name, login, crypt_algo, iter_count)
+ VALUES ('sip2', 'SIP2 Client Password', FALSE, 'bf', 5);
+
+-- ID 1 is magic.
+INSERT INTO sip.setting_group (id, label, institution)
+ VALUES (1, 'Default Settings', 'example');
+
+-- carve space for other canned setting groups
+SELECT SETVAL('sip.setting_group_id_seq'::TEXT, 1000);
+
+-- has to be global since settings are linked to accounts and if
+-- status-before-login is used, no account information will be available.
+INSERT INTO config.global_flag (name, value, enabled, label) VALUES
+( 'sip.sc_status_before_login_institution', NULL, FALSE,
+ oils_i18n_gettext(
+ 'sip.sc_status_before_login_institution',
+ 'Activate status-before-login-support and define the institution ' ||
+ 'value which should be used in the response',
+ 'cgf', 'label')
+);
+
+INSERT INTO sip.setting (setting_group, name, value, description)
+VALUES (
+ 1, 'currency', '"USD"',
+ oils_i18n_gettext(
+ (SELECT id FROM sip.setting WHERE name = 'currency'),
+ 'Monetary amounts are reported in this currency',
+ 'sipset', 'description')
+), (
+ 1, 'av_format', '"eg_legacy"',
+ oils_i18n_gettext(
+ (SELECT id FROM sip.setting WHERE name = 'av_format'),
+ 'AV Format. Options: eg_legacy, 3m, swyer_a, swyer_b',
+ 'sipset', 'description')
+), (
+ 1, 'due_date_use_sip_date_format', 'false',
+ oils_i18n_gettext(
+ (SELECT id FROM sip.setting WHERE name = 'due_date_use_sip_date_format'),
+ 'Due date uses 18-char date format (YYYYMMDDZZZZHHMMSS). Otherwise "YYYY-MM-DD HH:MM:SS',
+ 'sipset', 'description')
+), (
+ 1, 'patron_status_permit_loans', 'false',
+ oils_i18n_gettext(
+ (SELECT id FROM sip.setting WHERE name = 'patron_status_permit_loans'),
+ 'Checkout and renewal are allowed even when penalties blocking these actions exist',
+ 'sipset', 'description')
+), (
+ 1, 'patron_status_permit_all', 'false',
+ oils_i18n_gettext(
+ (SELECT id FROM sip.setting WHERE name = 'patron_status_permit_all'),
+ 'Holds, checkouts, and renewals allowed regardless of blocking penalties',
+ 'sipset', 'description')
+), (
+ 1, 'default_activity_who', 'null',
+ oils_i18n_gettext(
+ (SELECT id FROM sip.setting WHERE name = 'default_activity_who'),
+ 'Patron holds data may be returned as either "title" or "barcode"',
+ 'sipset', 'description')
+), (
+ 1, 'msg64_summary_datatype', '"title"',
+ oils_i18n_gettext(
+ (SELECT id FROM sip.setting WHERE name = 'msg64_summary_datatype'),
+ 'Patron circulation data may be returned as either "title" or "barcode"',
+ 'sipset', 'description')
+), (
+ 1, 'msg64_hold_items_available', '"title"',
+ oils_i18n_gettext(
+ (SELECT id FROM sip.setting WHERE name = 'msg64_hold_items_available'),
+ 'Patron holds data may be returned as either "title" or "barcode"',
+ 'sipset', 'description')
+), (
+ 1, 'checkout.override.COPY_ALERT_MESSAGE', 'true',
+ oils_i18n_gettext(
+ (SELECT id FROM sip.setting WHERE name = 'checkout.override.COPY_ALERT_MESSAGE'),
+ 'Checkout override copy alert message',
+ 'sipset', 'description')
+), (
+ 1, 'checkout.override.COPY_NOT_AVAILABLE', 'true',
+ oils_i18n_gettext(
+ (SELECT id FROM sip.setting WHERE name = 'checkout.override.COPY_NOT_AVAILABLE'),
+ 'Checkout override copy not available message',
+ 'sipset', 'description')
+), (
+ 1, 'checkin.override.COPY_ALERT_MESSAGE', 'true',
+ oils_i18n_gettext(
+ (SELECT id FROM sip.setting WHERE name = 'checkin.override.COPY_ALERT_MESSAGE'),
+ 'Checkin override copy alert message',
+ 'sipset', 'description')
+), (
+ 1, 'checkin.override.COPY_BAD_STATUS', 'true',
+ oils_i18n_gettext(
+ (SELECT id FROM sip.setting WHERE name = 'checkin.override.COPY_BAD_STATUS'),
+ 'Checkin override bad copy status',
+ 'sipset', 'description')
+), (
+ 1, 'checkin.override.COPY_STATUS_MISSING', 'true',
+ oils_i18n_gettext(
+ (SELECT id FROM sip.setting WHERE name = 'checkin.override.COPY_STATUS_MISSING'),
+ 'Checkin override copy status missing',
+ 'sipset', 'description')
+), (
+ 1, 'checkin_hold_as_transit', 'false',
+ oils_i18n_gettext(
+ (SELECT id FROM sip.setting WHERE name = 'checkin_hold_as_transit'),
+ 'Checkin local holds as transits',
+ 'sipset', 'description')
+);
+
+INSERT INTO sip.screen_message (key, message) VALUES (
+ 'checkout.open_circ_exists',
+ oils_i18n_gettext(
+ 'checkout.open_circ_exists',
+ 'This item is already checked out',
+ 'sipsm', 'message')
+), (
+ 'checkout.patron_not_allowed',
+ oils_i18n_gettext(
+ 'checkout.patron_not_allowed',
+ 'Patron is not allowed to checkout the selected item',
+ 'sipsm', 'message')
+), (
+ 'payment.overpayment_not_allowed',
+ oils_i18n_gettext(
+ 'payment.overpayment_not_allowed',
+ 'Overpayment not allowed',
+ 'sipsm', 'message')
+), (
+ 'payment.transaction_not_found',
+ oils_i18n_gettext(
+ 'payment.transaction_not_found',
+ 'Bill not found',
+ 'sipsm', 'message')
+);
+
+
+/* EXAMPLE SETTINGS
+
+-- Example linking a SIP password to the 'admin' account.
+SELECT actor.set_passwd(1, 'sip2', 'sip_password');
+
+INSERT INTO actor.workstation (name, owning_lib) VALUES ('BR1-SIP2-Gateway', 4);
+
+INSERT INTO sip.account(
+ setting_group, sip_username, sip_password, usr, workstation
+) VALUES (
+ 1, 'admin',
+ (SELECT id FROM actor.passwd WHERE usr = 1 AND passwd_type = 'sip2'),
+ 1,
+ (SELECT id FROM actor.workstation WHERE name = 'BR1-SIP2-Gateway')
+);
+
+*/
+
+COMMIT;
+
+
--- /dev/null
+SIP2Mediator Support
+^^^^^^^^^^^^^^^^^^^^
+
+Evergreen now supports back-end functionality to integrate with SIP2Mediator.
+
+For more information, see
+https://wiki.evergreen-ils.org/doku.php?id=evergreen-admin:sip2mediator[Evergreen Wiki].
+
+
+New Admin Interfaces
+++++++++++++++++++++
+
+* Manage SIP accounts:
+ ** Administration => Server Administration => SIP Accounts
+
+* Manage SIP Screen Messages:
+ ** Administration => Server Administration => SIP Screen Messages
+
+