More stuff from Evergreen
authorMike Rylander <mrylander@gmail.com>
Tue, 2 Apr 2013 23:57:47 +0000 (19:57 -0400)
committerMike Rylander <mrylander@gmail.com>
Tue, 2 Apr 2013 23:57:47 +0000 (19:57 -0400)
Signed-off-by: Mike Rylander <mrylander@gmail.com>

src/perl/lib/Fieldmapper.pm [new file with mode: 0644]
src/perl/lib/ShareStuff/Actor.pm [new file with mode: 0644]
src/perl/lib/ShareStuff/AppUtils.pm [new file with mode: 0644]
src/perl/lib/ShareStuff/CStoreEditor.pm [new file with mode: 0644]
src/perl/lib/ShareStuff/Const.pm [new file with mode: 0644]
src/perl/lib/ShareStuff/Event.pm [new file with mode: 0644]
src/perl/lib/ShareStuff/UI.pm

diff --git a/src/perl/lib/Fieldmapper.pm b/src/perl/lib/Fieldmapper.pm
new file mode 100644 (file)
index 0000000..709ad5c
--- /dev/null
@@ -0,0 +1,458 @@
+package Fieldmapper;
+use OpenSRF::Utils::JSON;
+use Data::Dumper;
+use OpenSRF::Utils::Logger;
+use OpenSRF::Utils::SettingsClient;
+use OpenSRF::System;
+use XML::LibXML;
+use Scalar::Util 'blessed';
+
+my $log = 'OpenSRF::Utils::Logger';
+
+use vars qw/$fieldmap $VERSION/;
+
+#
+# To dump the Javascript version of the fieldmapper struct use the command:
+#
+#      PERL5LIB=:~/vcs/ILS/Open-ILS/src/perlmods/lib/ GEN_JS=1 perl -MOpenILS::Utils::Fieldmapper -e 'print "\n";'
+#
+# ... adjusted for your VCS sandbox of choice, of course.
+#
+
+sub classes {
+       return () unless (defined $fieldmap);
+       return keys %$fieldmap;
+}
+
+# Find a Fieldmapper class given the json hint.
+sub class_for_hint {
+    my $hint = shift;
+    foreach (keys %$fieldmap) {
+        return $_ if ($fieldmap->{$_}->{hint} eq $hint);
+    }
+    return undef;
+}
+
+sub get_attribute {
+       my $attr_list = shift;
+       my $attr_name = shift;
+
+       my $attr = $attr_list->getNamedItem( $attr_name );
+       if( defined( $attr ) ) {
+               return $attr->getValue();
+       }
+       return undef;
+}
+
+sub load_fields {
+       my $field_list = shift;
+       my $fm = shift;
+
+       # Get attributes of the field list.  Since there is only one
+       # <field> per class, these attributes logically belong to the
+       # enclosing class, and that's where we load them.
+
+       my $field_attr_list = $field_list->attributes();
+
+       my $sequence  = get_attribute( $field_attr_list, 'oils_persist:sequence' );
+       if( ! defined( $sequence ) ) {
+               $sequence = '';
+       }
+       my $primary   = get_attribute( $field_attr_list, 'oils_persist:primary' );
+
+       # Load attributes into the Fieldmapper ----------------------
+
+       $$fieldmap{$fm}{ sequence } = $sequence;
+       $$fieldmap{$fm}{ identity } = $primary;
+
+       # Load each field -------------------------------------------
+
+       my $array_position = 0;
+       for my $field ( $field_list->childNodes() ) {    # For each <field>
+               if( $field->nodeName eq 'field' ) {
+       
+                       my $attribute_list = $field->attributes();
+                       
+                       my $name     = get_attribute( $attribute_list, 'name' );
+                       next if( $name eq 'isnew' || $name eq 'ischanged' || $name eq 'isdeleted' );
+                       my $required  = get_attribute( $attribute_list, 'oils_obj:required' );
+                       my $validate  = get_attribute( $attribute_list, 'oils_obj:validate' );
+                       my $virtual  = get_attribute( $attribute_list, 'oils_persist:virtual' );
+                       if( ! defined( $virtual ) ) {
+                               $virtual = "false";
+                       }
+                       my $selector = get_attribute( $attribute_list, 'reporter:selector' );
+
+                       $$fieldmap{$fm}{fields}{ $name } =
+                               { virtual => ( $virtual eq 'true' ) ? 1 : 0,
+                                 required => ( $required eq 'true' ) ? 1 : 0,
+                                 position => $array_position,
+                               };
+
+                       $$fieldmap{$fm}{fields}{ $name }{validate} = qr/$validate/ if (defined($validate));
+
+                       # The selector attribute, if present at all, attaches to only one
+                       # of the fields in a given class.  So if we see it, we store it at
+                       # the level of the enclosing class.
+
+                       if( defined( $selector ) ) {
+                               $$fieldmap{$fm}{selector} = $selector;
+                       }
+
+                       ++$array_position;
+               }
+       }
+
+       # Load the standard 3 virtual fields ------------------------
+
+       for my $vfield ( qw/isnew ischanged isdeleted/ ) {
+               $$fieldmap{$fm}{fields}{ $vfield } =
+                       { position => $array_position,
+                         virtual => 1
+                       };
+               ++$array_position;
+       }
+}
+
+sub load_links {
+       my $link_list = shift;
+       my $fm = shift;
+
+       for my $link ( $link_list->childNodes() ) {    # For each <link>
+               if( $link->nodeName eq 'link' ) {
+                       my $attribute_list = $link->attributes();
+                       
+                       my $field   = get_attribute( $attribute_list, 'field' );
+                       my $reltype = get_attribute( $attribute_list, 'reltype' );
+                       my $key     = get_attribute( $attribute_list, 'key' );
+                       my $class   = get_attribute( $attribute_list, 'class' );
+                       my $map     = get_attribute( $attribute_list, 'map' );
+
+                       $$fieldmap{$fm}{links}{ $field } =
+                               { class   => $class,
+                                 reltype => $reltype,
+                                 key     => $key,
+                                 map     => $map
+                               };
+               }
+       }
+}
+
+sub load_class {
+       my $class_node = shift;
+
+       # Get attributes ---------------------------------------------
+
+       my $attribute_list = $class_node->attributes();
+
+       my $fm               = get_attribute( $attribute_list, 'oils_obj:fieldmapper' );
+       $fm                  = 'Fieldmapper::' . $fm;
+       my $id               = get_attribute( $attribute_list, 'id' );
+       my $controller       = get_attribute( $attribute_list, 'controller' );
+       my $virtual          = get_attribute( $attribute_list, 'virtual' );
+       if( ! defined( $virtual ) ) {
+               $virtual = 'false';
+       }
+       my $tablename        = get_attribute( $attribute_list, 'oils_persist:tablename' );
+       if( ! defined( $tablename ) ) {
+               $tablename = '';
+       }
+       my $restrict_primary = get_attribute( $attribute_list, 'oils_persist:restrict_primary' );
+       my $field_safe = get_attribute( $attribute_list, 'oils_persist:field_safe' );
+
+       # Load the attributes into the Fieldmapper --------------------
+
+       $log->debug("Building Fieldmapper class for [$fm] from IDL");
+
+       $$fieldmap{$fm}{ hint }             = $id;
+       $$fieldmap{$fm}{ virtual }          = ( $virtual eq 'true' ) ? 1 : 0;
+       $$fieldmap{$fm}{ table }            = $tablename;
+       $$fieldmap{$fm}{ controller }       = [ split ' ', $controller ];
+       $$fieldmap{$fm}{ restrict_primary } = $restrict_primary;
+       $$fieldmap{$fm}{ field_safe }       = $field_safe;
+
+       # Load fields and links
+
+       for my $child ( $class_node->childNodes() ) {
+               my $nodeName = $child->nodeName;
+               if( $nodeName eq 'fields' ) {
+                       load_fields( $child, $fm );
+               } elsif( $nodeName eq 'links' ) {
+                       load_links( $child, $fm );
+               }
+       }
+}
+
+import();
+sub import {
+       my $class = shift;
+       my %args = @_;
+
+       return if (keys %$fieldmap);
+       return if (!OpenSRF::System->connected && !$args{IDL});
+
+       # parse the IDL ...
+       my $parser = XML::LibXML->new();
+       my $file = $args{IDL} || OpenSRF::Utils::SettingsClient->new->config_value( 'IDL' );
+       my $fmdoc = $parser->parse_file( $file );
+       my $rootnode = $fmdoc->documentElement();
+
+       for my $child ( $rootnode->childNodes() ) {    # For each <class>
+               my $nodeName = $child->nodeName;
+               if( $nodeName eq 'class' ) {
+                       load_class( $child );
+               }
+       }
+
+       #-------------------------------------------------------------------------------
+       # Now comes the evil!  Generate classes
+
+       for my $pkg ( __PACKAGE__->classes ) {
+               (my $cdbi = $pkg) =~ s/^Fieldmapper:://o;
+
+               eval <<"                PERL";
+                       package $pkg;
+                       use base 'Fieldmapper';
+               PERL
+
+               if (exists $$fieldmap{$pkg}{proto_fields}) {
+                       for my $pfield ( sort keys %{ $$fieldmap{$pkg}{proto_fields} } ) {
+                               $$fieldmap{$pkg}{fields}{$pfield} = { position => $pos, virtual => $$fieldmap{$pkg}{proto_fields}{$pfield} };
+                               $pos++;
+                       }
+               }
+
+               OpenSRF::Utils::JSON->register_class_hint(
+                       hint => $pkg->json_hint,
+                       name => $pkg,
+                       type => 'array',
+               );
+
+       }
+}
+
+sub new {
+       my $self = shift;
+       my $value = shift;
+       $value = [] unless (defined $value);
+       return bless $value => $self->class_name;
+}
+
+sub decast {
+       my $self = shift;
+       return [ @$self ];
+}
+
+sub DESTROY {}
+
+sub AUTOLOAD {
+       my $obj = shift;
+       my $value = shift;
+       (my $field = $AUTOLOAD) =~ s/^.*://o;
+       my $class_name = $obj->class_name;
+
+       my $fpos = $field;
+       $fpos  =~ s/^clear_//og ;
+
+       my $pos = $$fieldmap{$class_name}{fields}{$fpos}{position};
+
+       if ($field =~ /^clear_/o) {
+               {       no strict 'subs';
+                       *{$obj->class_name."::$field"} = sub {
+                               my $self = shift;
+                               $self->[$pos] = undef;
+                               return 1;
+                       };
+               }
+               return $obj->$field();
+       }
+
+       die "No field by the name $field in $class_name!"
+               unless (exists $$fieldmap{$class_name}{fields}{$field} && defined($pos));
+
+
+       {       no strict 'subs';
+               *{$obj->class_name."::$field"} = sub {
+                       my $self = shift;
+                       my $new_val = shift;
+                       $self->[$pos] = $new_val if (defined $new_val);
+                       return $self->[$pos];
+               };
+       }
+       return $obj->$field($value);
+}
+
+sub Selector {
+       my $self = shift;
+       return $$fieldmap{$self->class_name}{selector};
+}
+
+sub Identity {
+       my $self = shift;
+       return $$fieldmap{$self->class_name}{identity};
+}
+
+sub RestrictPrimary {
+       my $self = shift;
+       return $$fieldmap{$self->class_name}{restrict_primary};
+}
+
+sub Sequence {
+       my $self = shift;
+       return $$fieldmap{$self->class_name}{sequence};
+}
+
+sub Table {
+       my $self = shift;
+       return $$fieldmap{$self->class_name}{table};
+}
+
+sub Controller {
+       my $self = shift;
+       return $$fieldmap{$self->class_name}{controller};
+}
+
+sub RequiredField {
+       my $self = shift;
+       my $f = shift;
+    return undef unless ($f);
+       return $$fieldmap{$self->class_name}{fields}{$f}{required};
+}
+
+sub toXML {
+    my $self = shift;
+    return undef unless (ref $self);
+
+    my $opts = shift || {};
+    my $no_virt = $$opts{no_virt}; # skip virtual fields
+    my $skip_fields = $$opts{skip_fields} || {}; # eg. {au => ['passwd']}
+    my @to_skip = @{$$skip_fields{$self->json_hint}} 
+        if $$skip_fields{$self->json_hint};
+
+    my $dom = XML::LibXML::Document->new;
+    my $root = $dom->createElement( $self->json_hint );
+    $dom->setDocumentElement( $root );
+
+    my @field_names = $no_virt ? $self->real_fields : $self->properties;
+
+    for my $f (@field_names) {
+        next if ($f eq 'isnew');
+        next if ($f eq 'ischanged');
+        next if ($f eq 'isdeleted');
+        next if (grep {$_ eq $f} @to_skip);
+
+        my $value = $self->$f();
+        my $element = $dom->createElement( $f );
+
+        $value = [$value] if (blessed($value)); # fm object
+
+        if (ref($value)) { # array
+            for my $k (@$value) {
+                if (blessed($k)) {
+                    my $subdoc = $k->toXML($opts);
+                    next unless $subdoc;
+                    my $subnode = $subdoc->documentElement;
+                    $dom->adoptNode($subnode);
+                    $element->appendChild($subnode);
+                } elsif (ref $k) { # not sure what to do here
+                    $element->appendText($k);
+                } else { # meh .. just append, I guess
+                    $element->appendText($k);
+                }
+            }
+        } else {
+            $element->appendText($value);
+        }
+
+        $root->appendChild($element);
+    }
+
+    return $dom;
+}
+
+sub ValidateField {
+       my $self = shift;
+       my $f = shift;
+    return undef unless ($f);
+       return 1 if (!exists($$fieldmap{$self->class_name}{fields}{$f}{validate}));
+       return $self->$f =~ $$fieldmap{$self->class_name}{fields}{$f}{validate};
+}
+
+sub class_name {
+       my $class_name = shift;
+       return ref($class_name) || $class_name;
+}
+
+sub real_fields {
+       my $self = shift;
+       my $class_name = $self->class_name;
+       my $fields = $$fieldmap{$class_name}{fields};
+
+       my @f = grep {
+                       !$$fields{$_}{virtual}
+               } sort {$$fields{$a}{position} <=> $$fields{$b}{position}} keys %$fields;
+
+       return @f;
+}
+
+sub has_field {
+       my $self = shift;
+       my $field = shift;
+       my $class_name = $self->class_name;
+       return 1 if grep { $_ eq $field } keys %{$$fieldmap{$class_name}{fields}};
+       return 0;
+}
+
+sub properties {
+       my $self = shift;
+       my $class_name = $self->class_name;
+       return keys %{$$fieldmap{$class_name}{fields}};
+}
+
+sub to_bare_hash {
+       my $self = shift;
+
+       my %hash = ();
+       for my $f ($self->properties) {
+               my $val = $self->$f;
+               $hash{$f} = $val;
+       }
+
+       return \%hash;
+}
+
+sub clone {
+       my $self = shift;
+       return $self->new( [@$self] );
+}
+
+sub api_level {
+       my $self = shift;
+       return $fieldmap->{$self->class_name}->{api_level};
+}
+
+sub cdbi {
+       my $self = shift;
+       return $fieldmap->{$self->class_name}->{cdbi};
+}
+
+sub is_virtual {
+       my $self = shift;
+       my $field = shift;
+       return $fieldmap->{$self->class_name}->{proto_fields}->{$field} if ($field);
+       return $fieldmap->{$self->class_name}->{virtual};
+}
+
+sub is_readonly {
+       my $self = shift;
+       my $field = shift;
+       return $fieldmap->{$self->class_name}->{readonly};
+}
+
+sub json_hint {
+       my $self = shift;
+       return $fieldmap->{$self->class_name}->{hint};
+}
+
+
+1;
diff --git a/src/perl/lib/ShareStuff/Actor.pm b/src/perl/lib/ShareStuff/Actor.pm
new file mode 100644 (file)
index 0000000..289dcdd
--- /dev/null
@@ -0,0 +1,2487 @@
+package ShareStuff::Actor;
+use ShareStuff;
+use base qw/ShareStuff/;
+use strict; use warnings;
+use Data::Dumper;
+$Data::Dumper::Indent = 0;
+use ShareStuff::Event;
+
+use Digest::MD5 qw(md5_hex);
+
+use OpenSRF::EX qw(:try);
+use ShareStuff::AppUtils;
+
+use Fieldmapper;
+use OpenSRF::Utils::Logger qw/$logger/;
+use OpenSRF::Utils qw/:datetime/;
+use OpenSRF::Utils::SettingsClient;
+
+use OpenSRF::Utils::Cache;
+
+use OpenSRF::Utils::JSON;
+use DateTime;
+use DateTime::Format::ISO8601;
+use ShareStuff::Const qw/:const/;
+
+use ShareStuff::CStoreEditor qw/:funcs/;
+use List::Util qw/max reduce/;
+
+use UUID::Tiny qw/:std/;
+
+sub initialize {
+}
+
+my $apputils = "ShareStuff::AppUtils";
+my $U = $apputils;
+
+sub _d { warn "Patron:\n" . Dumper(shift()); }
+
+my $cache;
+my $set_user_settings;
+my $set_ou_settings;
+
+
+#__PACKAGE__->register_method(
+#      method  => "allowed_test",
+#      api_name        => "open-ils.actor.allowed_test",
+#);
+#sub allowed_test {
+#    my($self, $conn, $auth, $orgid, $permcode) = @_;
+#    my $e = new_editor(authtoken => $auth);
+#    return $e->die_event unless $e->checkauth;
+#
+#    return {
+#        orgid => $orgid,
+#        permcode => $permcode,
+#        result => $e->allowed($permcode, $orgid)
+#    };
+#}
+
+__PACKAGE__->register_method(
+       method  => "update_user_setting",
+       api_name        => "open-ils.actor.patron.settings.update",
+);
+sub update_user_setting {
+       my($self, $conn, $auth, $user_id, $settings) = @_;
+    my $e = new_editor(xact => 1, authtoken => $auth);
+    return $e->die_event unless $e->checkauth;
+
+    $user_id = $e->requestor->id unless defined $user_id;
+
+    unless($e->requestor->id == $user_id) {
+        my $user = $e->retrieve_actor_user($user_id) or return $e->die_event;
+        return $e->die_event unless $e->allowed('UPDATE_USER', $user->home_ou);
+    }
+
+    for my $name (keys %$settings) {
+        my $val = $$settings{$name};
+        my $set = $e->search_actor_user_setting({usr => $user_id, name => $name})->[0];
+
+        if(defined $val) {
+            $val = OpenSRF::Utils::JSON->perl2JSON($val);
+            if($set) {
+                $set->value($val);
+                $e->update_actor_user_setting($set) or return $e->die_event;
+            } else {
+                $set = Fieldmapper::actor::user_setting->new;
+                $set->usr($user_id);
+                $set->name($name);
+                $set->value($val);
+                $e->create_actor_user_setting($set) or return $e->die_event;
+            }
+        } elsif($set) {
+            $e->delete_actor_user_setting($set) or return $e->die_event;
+        }
+    }
+
+    $e->commit;
+    return 1;
+}
+
+
+__PACKAGE__->register_method(
+    method    => "set_ou_settings",
+    api_name  => "open-ils.actor.org_unit.settings.update",
+    signature => {
+        desc => "Updates the value for a given org unit setting.  The permission to update "          .
+                "an org unit setting is either the UPDATE_ORG_UNIT_SETTING_ALL, or a specific "       .
+                "permission specified in the update_perm column of the config.org_unit_setting_type " .
+                "table's row corresponding to the setting being changed." ,
+        params => [
+            {desc => 'Authentication token',             type => 'string'},
+            {desc => 'Org unit ID',                      type => 'number'},
+            {desc => 'Hash of setting name-value pairs', type => 'object'}
+        ],
+        return => {desc => '1 on success, Event on error'}
+    }
+);
+
+sub set_ou_settings {
+       my( $self, $client, $auth, $org_id, $settings ) = @_;
+
+    my $e = new_editor(authtoken => $auth, xact => 1);
+    return $e->die_event unless $e->checkauth;
+
+    my $all_allowed = $e->allowed("UPDATE_ORG_UNIT_SETTING_ALL", $org_id);
+
+       for my $name (keys %$settings) {
+        my $val = $$settings{$name};
+
+        my $type = $e->retrieve_config_org_unit_setting_type([
+            $name,
+            {flesh => 1, flesh_fields => {'coust' => ['update_perm']}}
+        ]) or return $e->die_event;
+        my $set = $e->search_actor_org_unit_setting({org_unit => $org_id, name => $name})->[0];
+
+        # If there is no relevant permission, the default assumption will
+        # be, "no, the caller cannot change that value."
+        return $e->die_event unless ($all_allowed ||
+            ($type->update_perm && $e->allowed($type->update_perm->code, $org_id)));
+
+        if(defined $val) {
+            $val = OpenSRF::Utils::JSON->perl2JSON($val);
+            if($set) {
+                $set->value($val);
+                $e->update_actor_org_unit_setting($set) or return $e->die_event;
+            } else {
+                $set = Fieldmapper::actor::org_unit_setting->new;
+                $set->org_unit($org_id);
+                $set->name($name);
+                $set->value($val);
+                $e->create_actor_org_unit_setting($set) or return $e->die_event;
+            }
+        } elsif($set) {
+            $e->delete_actor_org_unit_setting($set) or return $e->die_event;
+        }
+    }
+
+    $e->commit;
+    return 1;
+}
+
+__PACKAGE__->register_method(
+    method   => "user_settings",
+    authoritative => 1,
+    api_name => "open-ils.actor.patron.settings.retrieve",
+);
+sub user_settings {
+       my( $self, $client, $auth, $user_id, $setting ) = @_;
+
+    my $e = new_editor(authtoken => $auth);
+    return $e->event unless $e->checkauth;
+    $user_id = $e->requestor->id unless defined $user_id;
+
+    my $patron = $e->retrieve_actor_user($user_id) or return $e->event;
+    if($e->requestor->id != $user_id) {
+        return $e->event unless $e->allowed('VIEW_USER', $patron->home_ou);
+    }
+
+    sub get_setting {
+        my($e, $user_id, $setting) = @_;
+        my $val = $e->search_actor_user_setting({usr => $user_id, name => $setting})->[0];
+        return undef unless $val; # XXX this should really return undef, but needs testing
+        return OpenSRF::Utils::JSON->JSON2perl($val->value);
+    }
+
+    if($setting) {
+        if(ref $setting eq 'ARRAY') {
+            my %settings;
+            $settings{$_} = get_setting($e, $user_id, $_) for @$setting;
+            return \%settings;
+        } else {
+            return get_setting($e, $user_id, $setting);    
+        }
+    } else {
+        my $s = $e->search_actor_user_setting({usr => $user_id});
+           return { map { ( $_->name => OpenSRF::Utils::JSON->JSON2perl($_->value) ) } @$s };
+    }
+}
+
+
+__PACKAGE__->register_method(
+    method    => "ranged_ou_settings",
+    api_name  => "open-ils.actor.org_unit_setting.values.ranged.retrieve",
+    signature => {
+        desc   => "Retrieves all org unit settings for the given org_id, up to whatever limit " .
+                  "is implied for retrieving OU settings by the authenticated users' permissions.",
+        params => [
+            {desc => 'Authentication token',   type => 'string'},
+            {desc => 'Org unit ID',            type => 'number'},
+        ],
+        return => {desc => 'A hashref of "ranged" settings, event on error'}
+    }
+);
+sub ranged_ou_settings {
+       my( $self, $client, $auth, $org_id ) = @_;
+
+       my $e = new_editor(authtoken => $auth);
+    return $e->event unless $e->checkauth;
+
+    my %ranged_settings;
+    my $org_list = $U->get_org_ancestors($org_id);
+    my $settings = $e->search_actor_org_unit_setting({org_unit => $org_list});
+    $org_list = [ reverse @$org_list ];
+
+    # start at the context org and capture the setting value
+    # without clobbering settings we've already captured
+    for my $this_org_id (@$org_list) {
+        
+        my @sets = grep { $_->org_unit == $this_org_id } @$settings;
+
+        for my $set (@sets) {
+            my $type = $e->retrieve_config_org_unit_setting_type([
+                $set->name,
+                {flesh => 1, flesh_fields => {coust => ['view_perm']}}
+            ]);
+
+            # If there is no relevant permission, the default assumption will
+            # be, "yes, the caller can have that value."
+            if ($type && $type->view_perm) {
+                next if not $e->allowed($type->view_perm->code, $org_id);
+            }
+
+            $ranged_settings{$set->name} = OpenSRF::Utils::JSON->JSON2perl($set->value)
+                unless defined $ranged_settings{$set->name};
+        }
+    }
+
+       return \%ranged_settings;
+}
+
+
+
+__PACKAGE__->register_method(
+    api_name  => 'open-ils.actor.ou_setting.ancestor_default',
+    method    => 'ou_ancestor_setting',
+    signature => {
+        desc => 'Get the org unit setting value associated with the setting name as seen from the specified org unit.  ' .
+                'IF AND ONLY IF an authentication token is provided, this method will make sure that the given '         .
+                'user has permission to view that setting, if there is a permission associated with the setting.'        ,
+        params => [
+            { desc => 'Org unit ID',          type => 'number' },
+            { desc => 'setting name',         type => 'string' },
+            { desc => 'authtoken (optional)', type => 'string' }
+        ],
+        return => {desc => 'A value for the org unit setting, or undef'}
+    }
+);
+
+# ------------------------------------------------------------------
+# Attempts to find the org setting value for a given org.  if not 
+# found at the requested org, searches up the org tree until it 
+# finds a parent that has the requested setting.
+# when found, returns { org => $id, value => $value }
+# otherwise, returns NULL
+# ------------------------------------------------------------------
+sub ou_ancestor_setting {
+    my( $self, $client, $orgid, $name, $auth ) = @_;
+    return $U->ou_ancestor_setting($orgid, $name, undef, $auth);
+}
+
+__PACKAGE__->register_method(
+    api_name  => 'open-ils.actor.ou_setting.ancestor_default.batch',
+    method    => 'ou_ancestor_setting_batch',
+    signature => {
+        desc => 'Get org unit setting name => value pairs for a list of names, as seen from the specified org unit.  ' .
+                'IF AND ONLY IF an authentication token is provided, this method will make sure that the given '       .
+                'user has permission to view that setting, if there is a permission associated with the setting.'      ,
+        params => [
+            { desc => 'Org unit ID',          type => 'number' },
+            { desc => 'setting name list',    type => 'array'  },
+            { desc => 'authtoken (optional)', type => 'string' }
+        ],
+        return => {desc => 'A hash with name => value pairs for the org unit settings'}
+    }
+);
+sub ou_ancestor_setting_batch {
+    my( $self, $client, $orgid, $name_list, $auth ) = @_;
+    my %values;
+    $values{$_} = $U->ou_ancestor_setting($orgid, $_, undef, $auth) for @$name_list;
+    return \%values;
+}
+
+
+
+__PACKAGE__->register_method(
+    method   => "update_patron",
+    api_name => "open-ils.actor.patron.update",
+    signature => {
+        desc   => q/
+            Update an existing user, or create a new one.  Related objects,
+            like cards, addresses, survey responses, and stat cats, 
+            can be updated by attaching them to the user object in their
+            respective fields.  For examples, the billing address object
+            may be inserted into the 'billing_address' field, etc.  For each 
+            attached object, indicate if the object should be created, 
+            updated, or deleted using the built-in 'isnew', 'ischanged', 
+            and 'isdeleted' fields on the object.
+        /,
+        params => [
+            { desc => 'Authentication token', type => 'string' },
+            { desc => 'Patron data object',   type => 'object' }
+        ],
+        return => {desc => 'A fleshed user object, event on error'}
+    }
+);
+
+sub update_patron {
+       my( $self, $client, $user_session, $patron ) = @_;
+
+       my $session = $apputils->start_db_session();
+
+       $logger->info($patron->isnew ? "Creating new patron..." : "Updating Patron: " . $patron->id);
+
+       my( $user_obj, $evt ) = $U->checkses($user_session);
+       return $evt if $evt;
+
+       $evt = check_group_perm($session, $user_obj, $patron);
+       return $evt if $evt;
+
+       $apputils->set_audit_info($session, $user_session, $user_obj->id, $user_obj->wsid);
+
+       # $new_patron is the patron in progress.  $patron is the original patron
+       # passed in with the method.  new_patron will change as the components
+       # of patron are added/updated.
+
+       my $new_patron;
+
+       # create/update the patron first so we can use his id
+
+    # $patron is the obj from the client (new data) and $new_patron is the
+    # patron object properly built for db insertion, so we need a third variable
+    # if we want to represent the old patron.
+
+    my $old_patron;
+    my $barred_hook = '';
+
+       if($patron->isnew()) {
+               ( $new_patron, $evt ) = _add_patron($session, _clone_patron($patron), $user_obj);
+               return $evt if $evt;
+               if($U->is_true($patron->barred)) {
+                       $evt = $U->check_perms($user_obj->id, $patron->home_ou, 'BAR_PATRON');
+                       return $evt if $evt;
+               }
+       } else {
+        $new_patron = $patron;
+
+        # Did auth checking above already.
+        my $e = new_editor;
+        $old_patron = $e->retrieve_actor_user($patron->id) or
+            return $e->die_event;
+        $e->disconnect;
+        if($U->is_true($old_patron->barred) != $U->is_true($new_patron->barred)) {
+            $evt = $U->check_perms($user_obj->id, $patron->home_ou, $U->is_true($old_patron->barred) ? 'UNBAR_PATRON' : 'BAR_PATRON');
+            return $evt if $evt;
+
+            $barred_hook = $U->is_true($new_patron->barred) ? 
+                'au.barred' : 'au.unbarred';
+        }
+    }
+
+       ($new_patron, $evt) = _create_perm_maps($session, $user_session, $patron, $new_patron, $user_obj);
+       return $evt if $evt;
+
+       $apputils->commit_db_session($session);
+
+       return flesh_user($new_patron->id(), new_editor(requestor => $user_obj, xact => 1));
+}
+
+
+sub flesh_user {
+       my $id = shift;
+    my $e = shift;
+    my $home_ou = shift;
+
+    my $fields = [ "settings" ];
+    push @$fields, "home_ou" if $home_ou;
+    my $u = $e->retrieve_actor_user(
+    [
+        $id,
+        {
+            "flesh"             => 1,
+            "flesh_fields" =>  { "au" => $fields }
+        }
+    ]
+    ) or return $e->die_event;
+
+    return $u;
+}
+
+
+
+
+
+
+# clone and clear stuff that would break the database
+sub _clone_patron {
+       my $patron = shift;
+
+       my $new_patron = $patron->clone;
+       # clear these
+       $new_patron->clear_id();
+       $new_patron->clear_isnew();
+       $new_patron->clear_ischanged();
+       $new_patron->clear_isdeleted();
+       $new_patron->clear_permissions();
+
+       return $new_patron;
+}
+
+
+# XXX move to cstore...
+sub _add_patron {
+
+       my $session             = shift;
+       my $patron              = shift;
+       my $user_obj    = shift;
+
+       my $evt = $U->check_perms($user_obj->id, $patron->home_ou, 'CREATE_USER');
+       return (undef, $evt) if $evt;
+
+       my $ex = $session->request(
+               'open-ils.storage.direct.actor.user.search.usrname', $patron->usrname())->gather(1);
+       if( $ex and @$ex ) {
+               return (undef, ShareStuff::Event->new('USERNAME_EXISTS'));
+       }
+
+       $logger->info("Creating new user in the DB with username: ".$patron->usrname());
+
+       my $id = $session->request(
+               "open-ils.storage.direct.actor.user.create", $patron)->gather(1);
+       return (undef, $U->DB_UPDATE_FAILED($patron)) unless $id;
+
+       $logger->info("Successfully created new user [$id] in DB");
+
+       return ( $session->request( 
+               "open-ils.storage.direct.actor.user.retrieve", $id)->gather(1), undef );
+}
+
+
+sub check_group_perm {
+       my( $session, $requestor, $patron ) = @_;
+       my $evt;
+
+       # first let's see if the requestor has 
+       # priveleges to update this user in any way
+       if( ! $patron->isnew ) {
+               my $p = $session->request(
+                       'open-ils.storage.direct.actor.user.retrieve', $patron->id )->gather(1);
+
+               # If we are the requestor (trying to update our own account)
+               # and we are not trying to change our profile, we're good
+               if( $p->id == $requestor->id and 
+                               $p->profile == $patron->profile ) {
+                       return undef;
+               }
+
+
+               $evt = group_perm_failed($session, $requestor, $p);
+               return $evt if $evt;
+       }
+
+       # They are allowed to edit this patron.. can they put the 
+       # patron into the group requested?
+       $evt = group_perm_failed($session, $requestor, $patron);
+       return $evt if $evt;
+       return undef;
+}
+
+
+sub group_perm_failed {
+       my( $session, $requestor, $patron ) = @_;
+
+       my $perm;
+       my $grp;
+       my $grpid = $patron->profile;
+
+       do {
+
+               $logger->debug("user update looking for group perm for group $grpid");
+               $grp = $session->request(
+                       'open-ils.storage.direct.permission.grp_tree.retrieve', $grpid )->gather(1);
+               return ShareStuff::Event->new('PERMISSION_GRP_TREE_NOT_FOUND') unless $grp;
+
+       } while( !($perm = $grp->application_perm) and ($grpid = $grp->parent) );
+
+       $logger->info("user update checking perm $perm on user ".
+               $requestor->id." for update/create on user username=".$patron->usrname);
+
+       my $evt = $U->check_perms($requestor->id, $patron->home_ou, $perm);
+       return $evt if $evt;
+       return undef;
+}
+
+
+
+sub _update_patron {
+       my( $session, $patron, $user_obj, $noperm) = @_;
+
+       $logger->info("Updating patron ".$patron->id." in DB");
+
+       my $evt;
+
+       if(!$noperm) {
+               $evt = $U->check_perms($user_obj->id, $patron->home_ou, 'UPDATE_USER');
+               return (undef, $evt) if $evt;
+       }
+
+       # update the password by itself to avoid the password protection magic
+       if( $patron->passwd ) {
+               my $s = $session->request(
+                       'open-ils.storage.direct.actor.user.remote_update',
+                       {id => $patron->id}, {passwd => $patron->passwd})->gather(1);
+               return (undef, $U->DB_UPDATE_FAILED($patron)) unless defined($s);
+               $patron->clear_passwd;
+       }
+
+       if(!$patron->ident_type) {
+               $patron->clear_ident_type;
+               $patron->clear_ident_value;
+       }
+
+    $evt = verify_last_xact($session, $patron);
+    return (undef, $evt) if $evt;
+
+       my $stat = $session->request(
+               "open-ils.storage.direct.actor.user.update",$patron )->gather(1);
+       return (undef, $U->DB_UPDATE_FAILED($patron)) unless defined($stat);
+
+       return ($patron);
+}
+
+sub verify_last_xact {
+    my( $session, $patron ) = @_;
+    return undef unless $patron->id and $patron->id > 0;
+    my $p = $session->request(
+        'open-ils.storage.direct.actor.user.retrieve', $patron->id)->gather(1);
+    my $xact = $p->last_xact_id;
+    return undef unless $xact;
+    $logger->info("user xact = $xact, saving with xact " . $patron->last_xact_id);
+    return ShareStuff::Event->new('XACT_COLLISION')
+        if $xact ne $patron->last_xact_id;
+    return undef;
+}
+
+
+sub _check_dup_ident {
+       my( $session, $patron ) = @_;
+
+       return undef unless $patron->ident_value;
+
+       my $search = {
+               ident_type      => $patron->ident_type, 
+               ident_value => $patron->ident_value,
+       };
+
+       $logger->debug("patron update searching for dup ident values: " . 
+               $patron->ident_type . ':' . $patron->ident_value);
+
+       $search->{id} = {'!=' => $patron->id} if $patron->id and $patron->id > 0;
+
+       my $dups = $session->request(
+               'open-ils.storage.direct.actor.user.search_where.atomic', $search )->gather(1);
+
+
+       return ShareStuff::Event->new('PATRON_DUP_IDENT1', payload => $patron )
+               if $dups and @$dups;
+
+       return undef;
+}
+
+
+sub _add_update_addresses {
+
+       my $session = shift;
+       my $patron = shift;
+       my $new_patron = shift;
+
+       my $evt;
+
+       my $current_id; # id of the address before creation
+
+       my $addresses = $patron->addresses();
+
+       for my $address (@$addresses) {
+
+               next unless ref $address;
+               $current_id = $address->id();
+
+               if( $patron->billing_address() and
+                       $patron->billing_address() == $current_id ) {
+                       $logger->info("setting billing addr to $current_id");
+                       $new_patron->billing_address($address->id());
+                       $new_patron->ischanged(1);
+               }
+       
+               if( $patron->mailing_address() and
+                       $patron->mailing_address() == $current_id ) {
+                       $new_patron->mailing_address($address->id());
+                       $logger->info("setting mailing addr to $current_id");
+                       $new_patron->ischanged(1);
+               }
+
+
+               if($address->isnew()) {
+
+                       $address->usr($new_patron->id());
+
+                       ($address, $evt) = _add_address($session,$address);
+                       return (undef, $evt) if $evt;
+
+                       # we need to get the new id
+                       if( $patron->billing_address() and 
+                                       $patron->billing_address() == $current_id ) {
+                               $new_patron->billing_address($address->id());
+                               $logger->info("setting billing addr to $current_id");
+                               $new_patron->ischanged(1);
+                       }
+
+                       if( $patron->mailing_address() and
+                                       $patron->mailing_address() == $current_id ) {
+                               $new_patron->mailing_address($address->id());
+                               $logger->info("setting mailing addr to $current_id");
+                               $new_patron->ischanged(1);
+                       }
+
+               } elsif($address->ischanged() ) {
+
+                       ($address, $evt) = _update_address($session, $address);
+                       return (undef, $evt) if $evt;
+
+               } elsif($address->isdeleted() ) {
+
+                       if( $address->id() == $new_patron->mailing_address() ) {
+                               $new_patron->clear_mailing_address();
+                               ($new_patron, $evt) = _update_patron($session, $new_patron);
+                               return (undef, $evt) if $evt;
+                       }
+
+                       if( $address->id() == $new_patron->billing_address() ) {
+                               $new_patron->clear_billing_address();
+                               ($new_patron, $evt) = _update_patron($session, $new_patron);
+                               return (undef, $evt) if $evt;
+                       }
+
+                       $evt = _delete_address($session, $address);
+                       return (undef, $evt) if $evt;
+               } 
+       }
+
+       return ( $new_patron, undef );
+}
+
+
+# adds an address to the db and returns the address with new id
+sub _add_address {
+       my($session, $address) = @_;
+       $address->clear_id();
+
+       $logger->info("Creating new address at street ".$address->street1);
+
+       # put the address into the database
+       my $id = $session->request(
+               "open-ils.storage.direct.actor.user_address.create", $address )->gather(1);
+       return (undef, $U->DB_UPDATE_FAILED($address)) unless $id;
+
+       $address->id( $id );
+       return ($address, undef);
+}
+
+
+sub _update_address {
+       my( $session, $address ) = @_;
+
+       $logger->info("Updating address ".$address->id." in the DB");
+
+       my $stat = $session->request(
+               "open-ils.storage.direct.actor.user_address.update", $address )->gather(1);
+
+       return (undef, $U->DB_UPDATE_FAILED($address)) unless defined($stat);
+       return ($address, undef);
+}
+
+
+
+sub _add_update_cards {
+
+       my $session = shift;
+       my $patron = shift;
+       my $new_patron = shift;
+
+       my $evt;
+
+       my $virtual_id; #id of the card before creation
+
+       my $cards = $patron->cards();
+       for my $card (@$cards) {
+
+               $card->usr($new_patron->id());
+
+               if(ref($card) and $card->isnew()) {
+
+                       $virtual_id = $card->id();
+                       ( $card, $evt ) = _add_card($session,$card);
+                       return (undef, $evt) if $evt;
+
+                       #if(ref($patron->card)) { $patron->card($patron->card->id); }
+                       if($patron->card() == $virtual_id) {
+                               $new_patron->card($card->id());
+                               $new_patron->ischanged(1);
+                       }
+
+               } elsif( ref($card) and $card->ischanged() ) {
+                       $evt = _update_card($session, $card);
+                       return (undef, $evt) if $evt;
+               }
+       }
+
+       return ( $new_patron, undef );
+}
+
+
+# adds an card to the db and returns the card with new id
+sub _add_card {
+       my( $session, $card ) = @_;
+       $card->clear_id();
+
+       $logger->info("Adding new patron card ".$card->barcode);
+
+       my $id = $session->request(
+               "open-ils.storage.direct.actor.card.create", $card )->gather(1);
+       return (undef, $U->DB_UPDATE_FAILED($card)) unless $id;
+       $logger->info("Successfully created patron card $id");
+
+       $card->id($id);
+       return ( $card, undef );
+}
+
+
+# returns event on error.  returns undef otherwise
+sub _update_card {
+       my( $session, $card ) = @_;
+       $logger->info("Updating patron card ".$card->id);
+
+       my $stat = $session->request(
+               "open-ils.storage.direct.actor.card.update", $card )->gather(1);
+       return $U->DB_UPDATE_FAILED($card) unless defined($stat);
+       return undef;
+}
+
+
+
+
+# returns event on error.  returns undef otherwise
+sub _delete_address {
+       my( $session, $address ) = @_;
+
+       $logger->info("Deleting address ".$address->id." from DB");
+
+       my $stat = $session->request(
+               "open-ils.storage.direct.actor.user_address.delete", $address )->gather(1);
+
+       return $U->DB_UPDATE_FAILED($address) unless defined($stat);
+       return undef;
+}
+
+
+
+sub _add_survey_responses {
+       my ($session, $patron, $new_patron) = @_;
+
+       $logger->info( "Updating survey responses for patron ".$new_patron->id );
+
+       my $responses = $patron->survey_responses;
+
+       if($responses) {
+
+               $_->usr($new_patron->id) for (@$responses);
+
+               my $evt = $U->simplereq( "open-ils.circ", 
+                       "open-ils.circ.survey.submit.user_id", $responses );
+
+               return (undef, $evt) if defined($U->event_code($evt));
+
+       }
+
+       return ( $new_patron, undef );
+}
+
+sub _clear_badcontact_penalties {
+    my ($session, $old_patron, $new_patron, $user_obj) = @_;
+
+    return ($new_patron, undef) unless $old_patron;
+
+    my $PNM = $ShareStuff::BadContact::PENALTY_NAME_MAP;
+    my $e = new_editor(xact => 1);
+
+    # This ignores whether the caller of update_patron has any permission
+    # to remove penalties, but these penalties no longer make sense
+    # if an email address field (for example) is changed (and the caller must
+    # have perms to do *that*) so there's no reason not to clear the penalties.
+
+    my $bad_contact_penalties = $e->search_actor_user_standing_penalty([
+        {
+            "+csp" => {"name" => [values(%$PNM)]},
+            "+ausp" => {"stop_date" => undef, "usr" => $new_patron->id}
+        }, {
+            "join" => {"csp" => {}},
+            "flesh" => 1,
+            "flesh_fields" => {"ausp" => ["standing_penalty"]}
+        }
+    ]) or return (undef, $e->die_event);
+
+    return ($new_patron, undef) unless @$bad_contact_penalties;
+
+    my @penalties_to_clear;
+    my ($field, $penalty_name);
+
+    # For each field that might have an associated bad contact penalty, 
+    # check for such penalties and add them to the to-clear list if that
+    # field has changed.
+    while (($field, $penalty_name) = each(%$PNM)) {
+        if ($old_patron->$field ne $new_patron->$field) {
+            push @penalties_to_clear, grep {
+                $_->standing_penalty->name eq $penalty_name
+            } @$bad_contact_penalties;
+        }
+    }
+
+    foreach (@penalties_to_clear) {
+        # Note that this "archives" penalties, in the terminology of the staff
+        # client, instead of just deleting them.  This may assist reporting,
+        # or preserving old contact information when it is still potentially
+        # of interest.
+        $_->standing_penalty($_->standing_penalty->id); # deflesh
+        $_->stop_date('now');
+        $e->update_actor_user_standing_penalty($_) or return (undef, $e->die_event);
+    }
+
+    $e->commit;
+    return ($new_patron, undef);
+}
+
+
+sub _create_stat_maps {
+
+       my($session, $user_session, $patron, $new_patron) = @_;
+
+       my $maps = $patron->stat_cat_entries();
+
+       for my $map (@$maps) {
+
+               my $method = "open-ils.storage.direct.actor.stat_cat_entry_user_map.update";
+
+               if ($map->isdeleted()) {
+                       $method = "open-ils.storage.direct.actor.stat_cat_entry_user_map.delete";
+
+               } elsif ($map->isnew()) {
+                       $method = "open-ils.storage.direct.actor.stat_cat_entry_user_map.create";
+                       $map->clear_id;
+               }
+
+
+               $map->target_usr($new_patron->id);
+
+               #warn "
+               $logger->info("Updating stat entry with method $method and map $map");
+
+               my $stat = $session->request($method, $map)->gather(1);
+               return (undef, $U->DB_UPDATE_FAILED($map)) unless defined($stat);
+
+       }
+
+       return ($new_patron, undef);
+}
+
+sub _create_perm_maps {
+
+       my($session, $user_session, $patron, $new_patron) = @_;
+
+       my $maps = $patron->permissions;
+
+       for my $map (@$maps) {
+
+               my $method = "open-ils.storage.direct.permission.usr_perm_map.update";
+               if ($map->isdeleted()) {
+                       $method = "open-ils.storage.direct.permission.usr_perm_map.delete";
+               } elsif ($map->isnew()) {
+                       $method = "open-ils.storage.direct.permission.usr_perm_map.create";
+                       $map->clear_id;
+               }
+
+
+               $map->usr($new_patron->id);
+
+               #warn( "Updating permissions with method $method and session $user_session and map $map" );
+               $logger->info( "Updating permissions with method $method and map $map" );
+
+               my $stat = $session->request($method, $map)->gather(1);
+               return (undef, $U->DB_UPDATE_FAILED($map)) unless defined($stat);
+
+       }
+
+       return ($new_patron, undef);
+}
+
+
+__PACKAGE__->register_method(
+    method   => "set_user_work_ous",
+    api_name => "open-ils.actor.user.work_ous.update",
+);
+
+sub set_user_work_ous {
+    my $self   = shift;
+    my $client = shift;
+    my $ses    = shift;
+    my $maps   = shift;
+
+       my( $requestor, $evt ) = $apputils->checksesperm( $ses, 'ASSIGN_WORK_ORG_UNIT' );
+       return $evt if $evt;
+
+       my $session = $apputils->start_db_session();
+       $apputils->set_audit_info($session, $ses, $requestor->id, $requestor->wsid);
+
+       for my $map (@$maps) {
+
+               my $method = "open-ils.storage.direct.permission.usr_work_ou_map.update";
+               if ($map->isdeleted()) {
+                       $method = "open-ils.storage.direct.permission.usr_work_ou_map.delete";
+               } elsif ($map->isnew()) {
+                       $method = "open-ils.storage.direct.permission.usr_work_ou_map.create";
+                       $map->clear_id;
+               }
+
+               #warn( "Updating permissions with method $method and session $ses and map $map" );
+               $logger->info( "Updating work_ou map with method $method and map $map" );
+
+               my $stat = $session->request($method, $map)->gather(1);
+               $logger->warn( "update failed: ".$U->DB_UPDATE_FAILED($map) ) unless defined($stat);
+
+       }
+
+       $apputils->commit_db_session($session);
+
+       return scalar(@$maps);
+}
+
+
+__PACKAGE__->register_method(
+    method   => "set_user_perms",
+    api_name => "open-ils.actor.user.permissions.update",
+);
+
+sub set_user_perms {
+       my $self = shift;
+       my $client = shift;
+       my $ses = shift;
+       my $maps = shift;
+
+       my $session = $apputils->start_db_session();
+
+       my( $user_obj, $evt ) = $U->checkses($ses);
+       return $evt if $evt;
+       $apputils->set_audit_info($session, $ses, $user_obj->id, $user_obj->wsid);
+
+       my $perms = $session->request('open-ils.storage.permission.user_perms.atomic', $user_obj->id)->gather(1);
+
+       my $all = undef;
+       $all = 1 if ($U->is_true($user_obj->super_user()));
+    $all = 1 unless ($U->check_perms($user_obj->id, $user_obj->home_ou, 'EVERYTHING'));
+
+       for my $map (@$maps) {
+
+               my $method = "open-ils.storage.direct.permission.usr_perm_map.update";
+               if ($map->isdeleted()) {
+                       $method = "open-ils.storage.direct.permission.usr_perm_map.delete";
+               } elsif ($map->isnew()) {
+                       $method = "open-ils.storage.direct.permission.usr_perm_map.create";
+                       $map->clear_id;
+               }
+
+               next if (!$all and !grep { $_->perm eq $map->perm and $U->is_true($_->grantable) and $_->depth <= $map->depth } @$perms);
+               #warn( "Updating permissions with method $method and session $ses and map $map" );
+               $logger->info( "Updating permissions with method $method and map $map" );
+
+               my $stat = $session->request($method, $map)->gather(1);
+               $logger->warn( "update failed: ".$U->DB_UPDATE_FAILED($map) ) unless defined($stat);
+
+       }
+
+       $apputils->commit_db_session($session);
+
+       return scalar(@$maps);
+}
+
+
+__PACKAGE__->register_method(
+       method  => "user_retrieve_by_barcode",
+    authoritative => 1,
+       api_name        => "open-ils.actor.user.fleshed.retrieve_by_barcode",);
+
+sub user_retrieve_by_barcode {
+       my($self, $client, $auth, $barcode, $flesh_home_ou) = @_;
+
+    my $e = new_editor(authtoken => $auth);
+    return $e->event unless $e->checkauth;
+
+    my $card = $e->search_actor_card({barcode => $barcode})->[0]
+        or return $e->event;
+
+       my $user = flesh_user($card->usr, $e, $flesh_home_ou);
+    return $e->event unless $e->allowed(
+        "VIEW_USER", $flesh_home_ou ? $user->home_ou->id : $user->home_ou
+    );
+    return $user;
+}
+
+
+
+__PACKAGE__->register_method(
+    method        => "get_user_by_id",
+    authoritative => 1,
+    api_name      => "open-ils.actor.user.retrieve",
+);
+
+sub get_user_by_id {
+       my ($self, $client, $auth, $id) = @_;
+       my $e = new_editor(authtoken=>$auth);
+       return $e->event unless $e->checkauth;
+       my $user = $e->retrieve_actor_user($id) or return $e->event;
+       return $e->event unless $e->allowed('VIEW_USER', $user->home_ou);       
+       return $user;
+}
+
+
+__PACKAGE__->register_method(
+    method   => "get_org_types",
+    api_name => "open-ils.actor.org_types.retrieve",
+);
+sub get_org_types {
+    return $U->get_org_types();
+}
+
+
+__PACKAGE__->register_method(
+    method   => "get_user_ident_types",
+    api_name => "open-ils.actor.user.ident_types.retrieve",
+);
+my $ident_types;
+sub get_user_ident_types {
+       return $ident_types if $ident_types;
+       return $ident_types = 
+               new_editor()->retrieve_all_config_identification_type();
+}
+
+
+__PACKAGE__->register_method(
+    method   => "get_org_unit",
+    api_name => "open-ils.actor.org_unit.retrieve",
+);
+
+sub get_org_unit {
+       my( $self, $client, $user_session, $org_id ) = @_;
+       my $e = new_editor(authtoken => $user_session);
+       if(!$org_id) {
+               return $e->event unless $e->checkauth;
+               $org_id = $e->requestor->ws_ou;
+       }
+       my $o = $e->retrieve_actor_org_unit($org_id)
+               or return $e->event;
+       return $o;
+}
+
+__PACKAGE__->register_method(
+    method   => "search_org_unit",
+    api_name => "open-ils.actor.org_unit_list.search",
+);
+
+sub search_org_unit {
+
+       my( $self, $client, $field, $value ) = @_;
+
+       my $list = ShareStuff::AppUtils->simple_scalar_request(
+               "open-ils.cstore",
+               "open-ils.cstore.direct.actor.org_unit.search.atomic", 
+               { $field => $value } );
+
+       return $list;
+}
+
+
+# build the org tree
+
+__PACKAGE__->register_method(
+       method  => "get_org_tree",
+       api_name        => "open-ils.actor.org_tree.retrieve",
+       argc            => 0, 
+       note            => "Returns the entire org tree structure",
+);
+
+sub get_org_tree {
+       my $self = shift;
+       my $client = shift;
+       return $U->get_org_tree($client->session->session_locale);
+}
+
+
+__PACKAGE__->register_method(
+       method  => "get_org_descendants",
+       api_name        => "open-ils.actor.org_tree.descendants.retrieve"
+);
+
+# depth is optional.  org_unit is the id
+sub get_org_descendants {
+       my( $self, $client, $org_unit, $depth ) = @_;
+
+    if(ref $org_unit eq 'ARRAY') {
+        $depth ||= [];
+        my @trees;
+        for my $i (0..scalar(@$org_unit)-1) {
+            my $list = $U->simple_scalar_request(
+                           "open-ils.storage", 
+                           "open-ils.storage.actor.org_unit.descendants.atomic",
+                           $org_unit->[$i], $depth->[$i] );
+            push(@trees, $U->build_org_tree($list));
+        }
+        return \@trees;
+
+    } else {
+           my $orglist = $apputils->simple_scalar_request(
+                           "open-ils.storage", 
+                           "open-ils.storage.actor.org_unit.descendants.atomic",
+                           $org_unit, $depth );
+           return $U->build_org_tree($orglist);
+    }
+}
+
+
+__PACKAGE__->register_method(
+       method  => "get_org_ancestors",
+       api_name        => "open-ils.actor.org_tree.ancestors.retrieve"
+);
+
+# depth is optional.  org_unit is the id
+sub get_org_ancestors {
+       my( $self, $client, $org_unit, $depth ) = @_;
+       my $orglist = $apputils->simple_scalar_request(
+                       "open-ils.storage", 
+                       "open-ils.storage.actor.org_unit.ancestors.atomic",
+                       $org_unit, $depth );
+       return $U->build_org_tree($orglist);
+}
+
+
+__PACKAGE__->register_method(
+       method  => "get_standings",
+       api_name        => "open-ils.actor.standings.retrieve"
+);
+
+my $user_standings;
+sub get_standings {
+       return $user_standings if $user_standings;
+       return $user_standings = 
+               $apputils->simple_scalar_request(
+                       "open-ils.cstore",
+                       "open-ils.cstore.direct.config.standing.search.atomic",
+                       { id => { "!=" => undef } }
+               );
+}
+
+
+__PACKAGE__->register_method(
+    method   => "get_my_org_path",
+    api_name => "open-ils.actor.org_unit.full_path.retrieve"
+);
+
+sub get_my_org_path {
+       my( $self, $client, $auth, $org_id ) = @_;
+       my $e = new_editor(authtoken=>$auth);
+       return $e->event unless $e->checkauth;
+       $org_id = $e->requestor->ws_ou unless defined $org_id;
+
+       return $apputils->simple_scalar_request(
+               "open-ils.storage",
+               "open-ils.storage.actor.org_unit.full_path.atomic",
+               $org_id );
+}
+
+
+__PACKAGE__->register_method(
+    method   => "patron_adv_search",
+    api_name => "open-ils.actor.patron.search.advanced"
+);
+sub patron_adv_search {
+       my( $self, $client, $auth, $search_hash, 
+        $search_limit, $search_sort, $include_inactive, $search_ou ) = @_;
+
+       my $e = new_editor(authtoken=>$auth);
+       return $e->event unless $e->checkauth;
+       return $e->event unless $e->allowed('VIEW_USER');
+
+       # depth boundary outside of which patrons must opt-in, default to 0
+       my $opt_boundary = 0;
+       $opt_boundary = $U->ou_ancestor_setting_value($e->requestor->ws_ou,'org.patron_opt_boundary') if user_opt_in_enabled($self);
+
+       return $U->storagereq(
+               "open-ils.storage.actor.user.crazy_search", $search_hash, 
+            $search_limit, $search_sort, $include_inactive, $e->requestor->ws_ou, $search_ou, $opt_boundary);
+}
+
+
+__PACKAGE__->register_method(
+    method    => "update_passwd",
+    api_name  => "open-ils.actor.user.password.update",
+    signature => {
+        desc   => "Update the operator's password", 
+        params => [
+            { desc => 'Authentication token', type => 'string' },
+            { desc => 'New password',         type => 'string' },
+            { desc => 'Current password',     type => 'string' }
+        ],
+        return => {desc => '1 on success, Event on error or incorrect current password'}
+    }
+);
+
+__PACKAGE__->register_method(
+    method    => "update_passwd",
+    api_name  => "open-ils.actor.user.username.update",
+    signature => {
+        desc   => "Update the operator's username", 
+        params => [
+            { desc => 'Authentication token', type => 'string' },
+            { desc => 'New username',         type => 'string' },
+            { desc => 'Current password',     type => 'string' }
+        ],
+        return => {desc => '1 on success, Event on error or incorrect current password'}
+    }
+);
+
+__PACKAGE__->register_method(
+    method    => "update_passwd",
+    api_name  => "open-ils.actor.user.email.update",
+    signature => {
+        desc   => "Update the operator's email address", 
+        params => [
+            { desc => 'Authentication token', type => 'string' },
+            { desc => 'New email address',    type => 'string' },
+            { desc => 'Current password',     type => 'string' }
+        ],
+        return => {desc => '1 on success, Event on error or incorrect current password'}
+    }
+);
+
+sub update_passwd {
+    my( $self, $conn, $auth, $new_val, $orig_pw ) = @_;
+    my $e = new_editor(xact=>1, authtoken=>$auth);
+    return $e->die_event unless $e->checkauth;
+
+    my $db_user = $e->retrieve_actor_user($e->requestor->id)
+        or return $e->die_event;
+    my $api = $self->api_name;
+
+    # make sure the original password matches the in-database password
+    if (md5_hex($orig_pw) ne $db_user->passwd) {
+        $e->rollback;
+        return new ShareStuff::Event('INCORRECT_PASSWORD');
+    }
+
+    if( $api =~ /password/o ) {
+
+        $db_user->passwd($new_val);
+
+    } else {
+
+        # if we don't clear the password, the user will be updated with
+        # a hashed version of the hashed version of their password
+        $db_user->clear_passwd;
+
+        if( $api =~ /username/o ) {
+
+            # make sure no one else has this username
+            my $exist = $e->search_actor_user({usrname=>$new_val},{idlist=>1}); 
+            if (@$exist) {
+                $e->rollback;
+                return new ShareStuff::Event('USERNAME_EXISTS');
+            }
+            $db_user->usrname($new_val);
+
+        } elsif( $api =~ /email/o ) {
+            $db_user->email($new_val);
+        }
+    }
+
+    $e->update_actor_user($db_user) or return $e->die_event;
+    $e->commit;
+
+    # update the cached user to pick up these changes
+    $U->simplereq('open-ils.auth', 'open-ils.auth.session.reset_timeout', $auth, 1);
+    return 1;
+}
+
+
+
+__PACKAGE__->register_method(
+    method   => "check_user_perms",
+    api_name => "open-ils.actor.user.perm.check",
+    notes    => <<"    NOTES");
+       Takes a login session, user id, an org id, and an array of perm type strings.  For each
+       perm type, if the user does *not* have the given permission it is added
+       to a list which is returned from the method.  If all permissions
+       are allowed, an empty list is returned
+       if the logged in user does not match 'user_id', then the logged in user must
+       have VIEW_PERMISSION priveleges.
+       NOTES
+
+sub check_user_perms {
+       my( $self, $client, $login_session, $user_id, $org_id, $perm_types ) = @_;
+
+       my( $staff, $evt ) = $apputils->checkses($login_session);
+       return $evt if $evt;
+
+       if($staff->id ne $user_id) {
+               if( $evt = $apputils->check_perms(
+                       $staff->id, $org_id, 'VIEW_PERMISSION') ) {
+                       return $evt;
+               }
+       }
+
+       my @not_allowed;
+       for my $perm (@$perm_types) {
+               if($apputils->check_perms($user_id, $org_id, $perm)) {
+                       push @not_allowed, $perm;
+               }
+       }
+
+       return \@not_allowed
+}
+
+__PACKAGE__->register_method(
+       method  => "check_user_perms2",
+       api_name        => "open-ils.actor.user.perm.check.multi_org",
+       notes           => q/
+               Checks the permissions on a list of perms and orgs for a user
+               @param authtoken The login session key
+               @param user_id The id of the user to check
+               @param orgs The array of org ids
+               @param perms The array of permission names
+               @return An array of  [ orgId, permissionName ] arrays that FAILED the check
+               if the logged in user does not match 'user_id', then the logged in user must
+               have VIEW_PERMISSION priveleges.
+       /);
+
+sub check_user_perms2 {
+       my( $self, $client, $authtoken, $user_id, $orgs, $perms ) = @_;
+
+       my( $staff, $target, $evt ) = $apputils->checkses_requestor(
+               $authtoken, $user_id, 'VIEW_PERMISSION' );
+       return $evt if $evt;
+
+       my @not_allowed;
+       for my $org (@$orgs) {
+               for my $perm (@$perms) {
+                       if($apputils->check_perms($user_id, $org, $perm)) {
+                               push @not_allowed, [ $org, $perm ];
+                       }
+               }
+       }
+
+       return \@not_allowed
+}
+
+
+__PACKAGE__->register_method(
+       method => 'check_user_perms3',
+       api_name        => 'open-ils.actor.user.perm.highest_org',
+       notes           => q/
+               Returns the highest org unit id at which a user has a given permission
+               If the requestor does not match the target user, the requestor must have
+               'VIEW_PERMISSION' rights at the home org unit of the target user
+               @param authtoken The login session key
+               @param userid The id of the user in question
+               @param perm The permission to check
+               @return The org unit highest in the org tree within which the user has
+               the requested permission
+       /);
+
+sub check_user_perms3 {
+       my($self, $client, $authtoken, $user_id, $perm) = @_;
+       my $e = new_editor(authtoken=>$authtoken);
+       return $e->event unless $e->checkauth;
+
+       my $tree = $U->get_org_tree();
+
+    unless($e->requestor->id == $user_id) {
+        my $user = $e->retrieve_actor_user($user_id)
+            or return $e->event;
+        return $e->event unless $e->allowed('VIEW_PERMISSION', $user->home_ou);
+           return $U->find_highest_perm_org($perm, $user_id, $user->home_ou, $tree );
+    }
+
+    return $U->find_highest_perm_org($perm, $user_id, $e->requestor->ws_ou, $tree);
+}
+
+__PACKAGE__->register_method(
+       method => 'user_has_work_perm_at',
+       api_name        => 'open-ils.actor.user.has_work_perm_at',
+    authoritative => 1,
+    signature => {
+        desc => q/
+            Returns a set of org unit IDs which represent the highest orgs in 
+            the org tree where the user has the requested permission.  The
+            purpose of this method is to return the smallest set of org units
+            which represent the full expanse of the user's ability to perform
+            the requested action.  The user whose perms this method should
+            check is implied by the authtoken. /,
+        params => [
+                   {desc => 'authtoken', type => 'string'},
+            {desc => 'permission name', type => 'string'},
+            {desc => q/user id, optional.  If present, check perms for 
+                this user instead of the logged in user/, type => 'number'},
+        ],
+        return => {desc => 'An array of org IDs'}
+    }
+);
+
+sub user_has_work_perm_at {
+    my($self, $conn, $auth, $perm, $user_id) = @_;
+    my $e = new_editor(authtoken=>$auth);
+    return $e->event unless $e->checkauth;
+    if(defined $user_id) {
+        my $user = $e->retrieve_actor_user($user_id) or return $e->event;
+        return $e->event unless $e->allowed('VIEW_PERMISSION', $user->home_ou);
+    }
+    return $U->user_has_work_perm_at($e, $perm, undef, $user_id);
+}
+
+__PACKAGE__->register_method(
+       method => 'user_has_work_perm_at_batch',
+       api_name        => 'open-ils.actor.user.has_work_perm_at.batch',
+    authoritative => 1,
+);
+
+sub user_has_work_perm_at_batch {
+    my($self, $conn, $auth, $perms, $user_id) = @_;
+    my $e = new_editor(authtoken=>$auth);
+    return $e->event unless $e->checkauth;
+    if(defined $user_id) {
+        my $user = $e->retrieve_actor_user($user_id) or return $e->event;
+        return $e->event unless $e->allowed('VIEW_PERMISSION', $user->home_ou);
+    }
+    my $map = {};
+    $map->{$_} = $U->user_has_work_perm_at($e, $_) for @$perms;
+    return $map;
+}
+
+
+
+__PACKAGE__->register_method(
+       method => 'check_user_perms4',
+       api_name        => 'open-ils.actor.user.perm.highest_org.batch',
+       notes           => q/
+               Returns the highest org unit id at which a user has a given permission
+               If the requestor does not match the target user, the requestor must have
+               'VIEW_PERMISSION' rights at the home org unit of the target user
+               @param authtoken The login session key
+               @param userid The id of the user in question
+               @param perms An array of perm names to check 
+               @return An array of orgId's  representing the org unit 
+               highest in the org tree within which the user has the requested permission
+               The arrah of orgId's has matches the order of the perms array
+       /);
+
+sub check_user_perms4 {
+       my( $self, $client, $authtoken, $userid, $perms ) = @_;
+       
+       my( $staff, $target, $org, $evt );
+
+       ( $staff, $target, $evt ) = $apputils->checkses_requestor(
+               $authtoken, $userid, 'VIEW_PERMISSION' );
+       return $evt if $evt;
+
+       my @arr;
+       return [] unless ref($perms);
+       my $tree = $U->get_org_tree();
+
+       for my $p (@$perms) {
+               push( @arr, $U->find_highest_perm_org( $p, $userid, $target->home_ou, $tree ) );
+       }
+       return \@arr;
+}
+
+
+__PACKAGE__->register_method(
+    method        => "user_fines_summary",
+    api_name      => "open-ils.actor.user.fines.summary",
+    authoritative => 1,
+    signature     => {
+        desc   => 'Returns a short summary of the users total open fines, '  .
+                  'excluding voided fines Params are login_session, user_id' ,
+        params => [
+            {desc => 'Authentication token', type => 'string'},
+            {desc => 'User ID',              type => 'string'}  # number?
+        ],
+        return => {
+            desc => "a 'mous' object, event on error",
+        }
+    }
+);
+
+sub user_fines_summary {
+       my( $self, $client, $auth, $user_id ) = @_;
+
+       my $e = new_editor(authtoken=>$auth);
+       return $e->event unless $e->checkauth;
+
+       if( $user_id ne $e->requestor->id ) {
+           my $user = $e->retrieve_actor_user($user_id) or return $e->event;
+               return $e->event unless 
+                       $e->allowed('VIEW_USER_FINES_SUMMARY', $user->home_ou);
+       }
+
+    return $e->search_money_open_user_summary({usr => $user_id})->[0];
+}
+
+
+__PACKAGE__->register_method(
+    method        => "user_opac_vitals",
+    api_name      => "open-ils.actor.user.opac.vital_stats",
+    argc          => 1,
+    authoritative => 1,
+    signature     => {
+        desc   => 'Returns a short summary of the users vital stats, including '  .
+                  'identification information, accumulated balance, number of holds, ' .
+                  'and current open circulation stats' ,
+        params => [
+            {desc => 'Authentication token',                          type => 'string'},
+            {desc => 'Optional User ID, for use in the staff client', type => 'number'}  # number?
+        ],
+        return => {
+            desc => "An object with four properties: user, fines, checkouts and holds."
+        }
+    }
+);
+
+sub user_opac_vitals {
+       my( $self, $client, $auth, $user_id ) = @_;
+
+       my $e = new_editor(authtoken=>$auth);
+       return $e->event unless $e->checkauth;
+
+    $user_id ||= $e->requestor->id;
+
+    my $user = $e->retrieve_actor_user( $user_id );
+
+    my ($fines) = $self
+        ->method_lookup('open-ils.actor.user.fines.summary')
+        ->run($auth => $user_id);
+    return $fines if (defined($U->event_code($fines)));
+
+    if (!$fines) {
+        $fines = new Fieldmapper::money::open_user_summary ();
+        $fines->balance_owed(0.00);
+        $fines->total_owed(0.00);
+        $fines->total_paid(0.00);
+        $fines->usr($user_id);
+    }
+
+    my ($holds) = $self
+        ->method_lookup('open-ils.actor.user.hold_requests.count')
+        ->run($auth => $user_id);
+    return $holds if (defined($U->event_code($holds)));
+
+    my ($out) = $self
+        ->method_lookup('open-ils.actor.user.checked_out.count')
+        ->run($auth => $user_id);
+    return $out if (defined($U->event_code($out)));
+
+    $out->{"total_out"} = reduce { $a + $out->{$b} } 0, qw/out overdue long_overdue/;
+
+    return {
+        user => {
+            first_given_name  => $user->first_given_name,
+            second_given_name => $user->second_given_name,
+            family_name       => $user->family_name,
+            alias             => $user->alias,
+            usrname           => $user->usrname
+        },
+        fines => $fines->to_bare_hash,
+        checkouts => $out,
+        holds => $holds
+    };
+}
+
+
+##### a small consolidation of related method registrations
+my $common_params = [
+    { desc => 'Authentication token', type => 'string' },
+    { desc => 'User ID',              type => 'string' },
+    { desc => 'Transactions type (optional, defaults to all)', type => 'string' },
+    { desc => 'Options hash.  May contain limit and offset for paged results.', type => 'object' },
+];
+my %methods = (
+    'open-ils.actor.user.transactions'                      => '',
+    'open-ils.actor.user.transactions.fleshed'              => '',
+    'open-ils.actor.user.transactions.have_charge'          => ' that have an initial charge',
+    'open-ils.actor.user.transactions.have_charge.fleshed'  => ' that have an initial charge',
+    'open-ils.actor.user.transactions.have_balance'         => ' that have an outstanding balance',
+    'open-ils.actor.user.transactions.have_balance.fleshed' => ' that have an outstanding balance',
+);
+
+foreach (keys %methods) {
+    my %args = (
+        method    => "user_transactions",
+        api_name  => $_,
+        signature => {
+            desc   => 'For a given user, retrieve a list of '
+                    . (/\.fleshed/ ? 'fleshed ' : '')
+                    . 'transactions' . $methods{$_}
+                    . ' optionally limited to transactions of a given type.',
+            params => $common_params,
+            return => {
+                desc => "List of objects, or event on error.  Each object is a hash containing: transaction, circ, record. "
+                      . 'These represent the relevant (mbts) transaction, attached circulation and title pointed to in the circ, respectively.',
+            }
+        }
+    );
+    $args{authoritative} = 1;
+    __PACKAGE__->register_method(%args);
+}
+
+# Now for the counts
+%methods = (
+    'open-ils.actor.user.transactions.count'              => '',
+    'open-ils.actor.user.transactions.have_charge.count'  => ' that have an initial charge',
+    'open-ils.actor.user.transactions.have_balance.count' => ' that have an outstanding balance',
+);
+
+foreach (keys %methods) {
+    my %args = (
+        method    => "user_transactions",
+        api_name  => $_,
+        signature => {
+            desc   => 'For a given user, retrieve a count of open '
+                    . 'transactions' . $methods{$_}
+                    . ' optionally limited to transactions of a given type.',
+            params => $common_params,
+            return => { desc => "Integer count of transactions, or event on error" }
+        }
+    );
+    /\.have_balance/ and $args{authoritative} = 1;     # FIXME: I don't know why have_charge isn't authoritative
+    __PACKAGE__->register_method(%args);
+}
+
+__PACKAGE__->register_method(
+    method        => "user_transactions",
+    api_name      => "open-ils.actor.user.transactions.have_balance.total",
+    authoritative => 1,
+    signature     => {
+        desc   => 'For a given user, retrieve the total balance owed for open transactions,'
+                . ' optionally limited to transactions of a given type.',
+        params => $common_params,
+        return => { desc => "Decimal balance value, or event on error" }
+    }
+);
+
+
+sub user_transactions {
+       my( $self, $client, $auth, $user_id, $type, $options ) = @_;
+    $options ||= {};
+
+    my $e = new_editor(authtoken => $auth);
+    return $e->event unless $e->checkauth;
+
+    my $user = $e->retrieve_actor_user($user_id) or return $e->event;
+
+    return $e->event unless 
+        $e->requestor->id == $user_id or
+        $e->allowed('VIEW_USER_TRANSACTIONS', $user->home_ou);
+
+    my $api = $self->api_name();
+
+    my $filter = ($api =~ /have_balance/o) ?
+        { 'balance_owed' => { '<>' => 0 } }:
+        { 'total_owed' => { '>' => 0 } };
+
+    my $method = 'open-ils.actor.user.transactions.history.still_open';
+    $method = "$method.authoritative" if $api =~ /authoritative/;
+    my ($trans) = $self->method_lookup($method)->run($auth, $user_id, $type, $filter, $options);
+
+       if($api =~ /total/o) { 
+               my $total = 0.0;
+        $total += $_->balance_owed for @$trans;
+               return $total;
+       }
+
+    ($api =~ /count/o  ) and return scalar @$trans;
+    ($api !~ /fleshed/o) and return $trans;
+
+       my @resp;
+       for my $t (@$trans) {
+                       
+               if( $t->xact_type ne 'circulation' ) {
+                       push @resp, {transaction => $t};
+                       next;
+               }
+
+        my $circ_data = flesh_circ($e, $t->id);
+               push @resp, {transaction => $t, %$circ_data};
+       }
+
+       return \@resp; 
+} 
+
+
+__PACKAGE__->register_method(
+    method   => "user_transaction_retrieve",
+    api_name => "open-ils.actor.user.transaction.fleshed.retrieve",
+    argc     => 1,
+    authoritative => 1,
+    notes    => "Returns a fleshed transaction record"
+);
+
+__PACKAGE__->register_method(
+    method   => "user_transaction_retrieve",
+    api_name => "open-ils.actor.user.transaction.retrieve",
+    argc     => 1,
+    authoritative => 1,
+    notes    => "Returns a transaction record"
+);
+
+sub user_transaction_retrieve {
+       my($self, $client, $auth, $bill_id) = @_;
+
+    my $e = new_editor(authtoken => $auth);
+    return $e->event unless $e->checkauth;
+
+    my $trans = $e->retrieve_money_billable_transaction_summary(
+        [$bill_id, {flesh => 1, flesh_fields => {mbts => ['usr']}}]) or return $e->event;
+
+    return $e->event unless $e->allowed('VIEW_USER_TRANSACTIONS', $trans->usr->home_ou);
+
+    $trans->usr($trans->usr->id); # de-flesh for backwards compat
+
+    return $trans unless $self->api_name =~ /flesh/;
+    return {transaction => $trans} if $trans->xact_type ne 'circulation';
+
+    my $circ_data = flesh_circ($e, $trans->id, 1);
+
+       return {transaction => $trans, %$circ_data};
+}
+
+sub flesh_circ {
+    my $e = shift;
+    my $circ_id = shift;
+    my $flesh_copy = shift;
+
+    my $circ = $e->retrieve_action_circulation([
+        $circ_id, {
+            flesh => 3,
+            flesh_fields => {
+                circ => ['target_copy'],
+                acp => ['call_number'],
+                acn => ['record']
+            }
+        }
+    ]);
+
+       my $mods;
+    my $copy = $circ->target_copy;
+
+    if($circ->target_copy->call_number->id == OILS_PRECAT_CALL_NUMBER) {
+        $mods = new Fieldmapper::metabib::virtual_record;
+        $mods->doc_id(OILS_PRECAT_RECORD);
+        $mods->title($copy->dummy_title);
+        $mods->author($copy->dummy_author);
+
+    } else {
+        $mods = $U->record_to_mvr($circ->target_copy->call_number->record);
+    }
+
+    # more de-fleshiing
+    $circ->target_copy($circ->target_copy->id);
+    $copy->call_number($copy->call_number->id);
+
+       return {circ => $circ, record => $mods, copy => ($flesh_copy) ? $copy : undef };
+}
+
+
+__PACKAGE__->register_method(
+    method        => "hold_request_count",
+    api_name      => "open-ils.actor.user.hold_requests.count",
+    authoritative => 1,
+    argc          => 1,
+    notes         => 'Returns hold ready/total counts'
+);
+       
+sub hold_request_count {
+       my( $self, $client, $authtoken, $user_id ) = @_;
+    my $e = new_editor(authtoken => $authtoken);
+    return $e->event unless $e->checkauth;
+
+    $user_id = $e->requestor->id unless defined $user_id;
+
+    if($e->requestor->id ne $user_id) {
+        my $user = $e->retrieve_actor_user($user_id);
+        return $e->event unless $e->allowed('VIEW_HOLD', $user->home_ou);
+    }
+
+    my $holds = $e->json_query({
+        select => {ahr => ['pickup_lib', 'current_shelf_lib']},
+        from => 'ahr',
+        where => {
+            usr => $user_id,
+            fulfillment_time => {"=" => undef },
+            cancel_time => undef,
+        }
+    });
+
+       return { 
+        total => scalar(@$holds), 
+        ready => scalar(
+            grep { 
+                $_->{current_shelf_lib} and # avoid undef warnings
+                $_->{pickup_lib} eq $_->{current_shelf_lib} 
+            } @$holds
+        ) 
+    };
+}
+
+__PACKAGE__->register_method(
+    method        => "checked_out",
+    api_name      => "open-ils.actor.user.checked_out",
+    authoritative => 1,
+    argc          => 2,
+       signature     => {
+        desc => "For a given user, returns a structure of circulations objects sorted by out, overdue, lost, claims_returned, long_overdue. "
+              . "A list of IDs are returned of each type.  Circs marked lost, long_overdue, and claims_returned will not be 'finished' "
+              . "(i.e., outstanding balance or some other pending action on the circ). "
+              . "The .count method also includes a 'total' field which sums all open circs.",
+        params => [
+            { desc => 'Authentication Token', type => 'string'},
+            { desc => 'User ID',              type => 'string'},
+        ],
+        return => {
+            desc => 'Returns event on error, or an object with ID lists, like: '
+                  . '{"out":[12552,451232], "claims_returned":[], "long_overdue":[23421] "overdue":[], "lost":[]}'
+        },
+    }
+);
+
+__PACKAGE__->register_method(
+    method        => "checked_out",
+    api_name      => "open-ils.actor.user.checked_out.count",
+    authoritative => 1,
+    argc          => 2,
+    signature     => q/@see open-ils.actor.user.checked_out/
+);
+
+sub checked_out {
+       my( $self, $conn, $auth, $userid ) = @_;
+
+       my $e = new_editor(authtoken=>$auth);
+       return $e->event unless $e->checkauth;
+
+       if( $userid ne $e->requestor->id ) {
+        my $user = $e->retrieve_actor_user($userid) or return $e->event;
+               return $e->event unless($e->allowed('VIEW_CIRCULATIONS', $user->home_ou));
+       }
+
+       my $count = $self->api_name =~ /count/;
+       return _checked_out( $count, $e, $userid );
+}
+
+sub _checked_out {
+       my( $iscount, $e, $userid ) = @_;
+
+    my %result = (
+        out => [],
+        overdue => [],
+        lost => [],
+        claims_returned => [],
+        long_overdue => []
+    );
+       my $meth = 'retrieve_action_open_circ_';
+
+    if ($iscount) {
+           $meth .= 'count';
+        %result = (
+            out => 0,
+            overdue => 0,
+            lost => 0,
+            claims_returned => 0,
+            long_overdue => 0
+        );
+    } else {
+           $meth .= 'list';
+    }
+
+    my $data = $e->$meth($userid);
+
+    if ($data) {
+        if ($iscount) {
+            $result{$_} += $data->$_() for (keys %result);
+            $result{total} += $data->$_() for (keys %result);
+        } else {
+            for my $k (keys %result) {
+                $result{$k} = [ grep { $_ > 0 } split( ',', $data->$k()) ];
+            }
+        }
+    }
+
+    return \%result;
+}
+
+
+
+__PACKAGE__->register_method(
+    method        => "checked_in_with_fines",
+    api_name      => "open-ils.actor.user.checked_in_with_fines",
+    authoritative => 1,
+    argc          => 2,
+    signature     => q/@see open-ils.actor.user.checked_out/
+);
+
+sub checked_in_with_fines {
+       my( $self, $conn, $auth, $userid ) = @_;
+
+       my $e = new_editor(authtoken=>$auth);
+       return $e->event unless $e->checkauth;
+
+       if( $userid ne $e->requestor->id ) {
+               return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
+       }
+
+       # money is owed on these items and they are checked in
+       my $open = $e->search_action_circulation(
+               {
+                       usr                             => $userid, 
+                       xact_finish             => undef,
+                       checkin_time    => { "!=" => undef },
+               }
+       );
+
+
+       my( @lost, @cr, @lo );
+       for my $c (@$open) {
+               push( @lost, $c->id ) if $c->stop_fines eq 'LOST';
+               push( @cr, $c->id ) if $c->stop_fines eq 'CLAIMSRETURNED';
+               push( @lo, $c->id ) if $c->stop_fines eq 'LONGOVERDUE';
+       }
+
+       return {
+               lost            => \@lost,
+               claims_returned => \@cr,
+               long_overdue            => \@lo
+       };
+}
+
+
+sub _sigmaker {
+    my ($api, $desc, $auth) = @_;
+    $desc = $desc ? (" " . $desc) : '';
+    my $ids = ($api =~ /ids$/) ? 1 : 0;
+    my @sig = (
+        argc      => 1,
+        method    => "user_transaction_history",
+        api_name  => "open-ils.actor.user.transactions.$api",
+        signature => {
+            desc   => "For a given User ID, returns a list of billable transaction" .
+                      ($ids ? " id" : '') .
+                      "s$desc, optionally filtered by type and/or fields in money.billable_xact_summary.  " .
+                      "The VIEW_USER_TRANSACTIONS permission is required to view another user's transactions",
+            params => [
+                {desc => 'Authentication token',        type => 'string'},
+                {desc => 'User ID',                     type => 'number'},
+                {desc => 'Transaction type (optional)', type => 'number'},
+                {desc => 'Hash of Billable Transaction Summary filters (optional)', type => 'object'}
+            ],
+            return => {
+                desc => 'List of transaction' . ($ids ? " id" : '') . 's, Event on error'
+            },
+        }
+    );
+    $auth and push @sig, (authoritative => 1);
+    return @sig;
+}
+
+my %auth_hist_methods = (
+    'history'             => '',
+    'history.have_charge' => 'that have an initial charge',
+    'history.still_open'  => 'that are not finished',
+    'history.have_balance'         => 'that have a balance',
+    'history.have_bill'            => 'that have billings',
+    'history.have_bill_or_payment' => 'that have non-zero-sum billings or at least 1 payment',
+    'history.have_payment' => 'that have at least 1 payment',
+);
+
+foreach (keys %auth_hist_methods) {
+    __PACKAGE__->register_method(_sigmaker($_,       $auth_hist_methods{$_}, 1));
+    __PACKAGE__->register_method(_sigmaker("$_.ids", $auth_hist_methods{$_}, 1));
+    __PACKAGE__->register_method(_sigmaker("$_.fleshed", $auth_hist_methods{$_}, 1));
+}
+
+sub user_transaction_history {
+       my( $self, $conn, $auth, $userid, $type, $filter, $options ) = @_;
+    $filter ||= {};
+    $options ||= {};
+
+       my $e = new_editor(authtoken=>$auth);
+       return $e->die_event unless $e->checkauth;
+
+       if ($e->requestor->id ne $userid) {
+        return $e->die_event unless $e->allowed('VIEW_USER_TRANSACTIONS');
+       }
+
+       my $api = $self->api_name;
+       my @xact_finish  = (xact_finish => undef ) if ($api =~ /history\.still_open$/);     # What about history.still_open.ids?
+
+       if(defined($type)) {
+               $filter->{'xact_type'} = $type;
+       }
+
+       if($api =~ /have_bill_or_payment/o) {
+
+        # transactions that have a non-zero sum across all billings or at least 1 payment
+        $filter->{'-or'} = {
+            'balance_owed' => { '<>' => 0 },
+            'last_payment_ts' => { '<>' => undef }
+        };
+
+    } elsif($api =~ /have_payment/) {
+
+        $filter->{last_payment_ts} ||= {'<>' => undef};
+
+    } elsif( $api =~ /have_balance/o) {
+
+        # transactions that have a non-zero overall balance
+        $filter->{'balance_owed'} = { '<>' => 0 };
+
+       } elsif( $api =~ /have_charge/o) {
+
+        # transactions that have at least 1 billing, regardless of whether it was voided
+        $filter->{'last_billing_ts'} = { '<>' => undef };
+
+       } elsif( $api =~ /have_bill/o) {    # needs to be an elsif, or we double-match have_bill_or_payment!
+
+        # transactions that have non-zero sum across all billings.  This will exclude
+        # xacts where all billings have been voided
+        $filter->{'total_owed'} = { '<>' => 0 };
+       }
+
+    my $options_clause = { order_by => { mbt => 'xact_start DESC' } };
+    $options_clause->{'limit'} = $options->{'limit'} if $options->{'limit'}; 
+    $options_clause->{'offset'} = $options->{'offset'} if $options->{'offset'}; 
+
+    my $mbts = $e->search_money_billable_transaction_summary(
+        [   { usr => $userid, @xact_finish, %$filter },
+            $options_clause
+        ]
+    );
+
+    return [map {$_->id} @$mbts] if $api =~ /\.ids/;
+    return $mbts unless $api =~ /fleshed/;
+
+       my @resp;
+       for my $t (@$mbts) {
+                       
+               if( $t->xact_type ne 'circulation' ) {
+                       push @resp, {transaction => $t};
+                       next;
+               }
+
+        my $circ_data = flesh_circ($e, $t->id);
+               push @resp, {transaction => $t, %$circ_data};
+       }
+
+       return \@resp; 
+}
+
+
+__PACKAGE__->register_method(
+    method   => "retrieve_perms",
+    api_name => "open-ils.actor.permissions.retrieve",
+    notes    => "Returns a list of permissions"
+);
+sub retrieve_perms {
+       my( $self, $client ) = @_;
+       return $apputils->simple_scalar_request(
+               "open-ils.cstore",
+               "open-ils.cstore.direct.permission.perm_list.search.atomic",
+               { id => { '!=' => undef } }
+       );
+}
+
+__PACKAGE__->register_method(
+    method   => "retrieve_groups",
+    api_name => "open-ils.actor.groups.retrieve",
+    notes    => "Returns a list of user groups"
+);
+sub retrieve_groups {
+       my( $self, $client ) = @_;
+       return new_editor()->retrieve_all_permission_grp_tree();
+}
+
+__PACKAGE__->register_method(
+    method   => "retrieve_groups_tree",
+    api_name => "open-ils.actor.groups.tree.retrieve",
+    notes    => "Returns a list of user groups"
+);
+       
+sub retrieve_groups_tree {
+       my( $self, $client ) = @_;
+       return new_editor()->search_permission_grp_tree(
+               [
+                       { parent => undef},
+                       {       
+                               flesh                           => -1,
+                               flesh_fields    => { pgt => ["children"] }, 
+                               order_by                        => { pgt => 'name'}
+                       }
+               ]
+       )->[0];
+}
+
+
+__PACKAGE__->register_method(
+    method   => "add_user_to_groups",
+    api_name => "open-ils.actor.user.set_groups",
+    notes    => "Adds a user to one or more permission groups"
+);
+       
+sub add_user_to_groups {
+       my( $self, $client, $authtoken, $userid, $groups ) = @_;
+
+       my( $requestor, $target, $evt ) = $apputils->checkses_requestor(
+               $authtoken, $userid, 'CREATE_USER_GROUP_LINK' );
+       return $evt if $evt;
+
+       ( $requestor, $target, $evt ) = $apputils->checkses_requestor(
+               $authtoken, $userid, 'REMOVE_USER_GROUP_LINK' );
+       return $evt if $evt;
+
+       $apputils->simplereq(
+               'open-ils.storage',
+               'open-ils.storage.direct.permission.usr_grp_map.mass_delete', { usr => $userid } );
+               
+       for my $group (@$groups) {
+               my $link = Fieldmapper::permission::usr_grp_map->new;
+               $link->grp($group);
+               $link->usr($userid);
+
+               my $id = $apputils->simplereq(
+                       'open-ils.storage',
+                       'open-ils.storage.direct.permission.usr_grp_map.create', $link );
+       }
+
+       return 1;
+}
+
+__PACKAGE__->register_method(
+    method   => "get_user_perm_groups",
+    api_name => "open-ils.actor.user.get_groups",
+    notes    => "Retrieve a user's permission groups."
+);
+
+
+sub get_user_perm_groups {
+       my( $self, $client, $authtoken, $userid ) = @_;
+
+       my( $requestor, $target, $evt ) = $apputils->checkses_requestor(
+               $authtoken, $userid, 'VIEW_PERM_GROUPS' );
+       return $evt if $evt;
+
+       return $apputils->simplereq(
+               'open-ils.cstore',
+               'open-ils.cstore.direct.permission.usr_grp_map.search.atomic', { usr => $userid } );
+}      
+
+__PACKAGE__->register_method(
+    method        => 'fetch_patron_note',
+    api_name      => 'open-ils.actor.note.retrieve.all',
+    authoritative => 1,
+    signature     => q/
+               Returns a list of notes for a given user
+               Requestor must have VIEW_USER permission if pub==false and
+               @param authtoken The login session key
+               @param args Hash of params including
+                       patronid : the patron's id
+                       pub : true if retrieving only public notes
+       /
+);
+
+sub fetch_patron_note {
+       my( $self, $conn, $authtoken, $args ) = @_;
+       my $patronid = $$args{patronid};
+
+       my($reqr, $evt) = $U->checkses($authtoken);
+       return $evt if $evt;
+
+       my $patron;
+       ($patron, $evt) = $U->fetch_user($patronid);
+       return $evt if $evt;
+
+       if($$args{pub}) {
+               if( $patronid ne $reqr->id ) {
+                       $evt = $U->check_perms($reqr->id, $patron->home_ou, 'VIEW_USER');
+                       return $evt if $evt;
+               }
+               return $U->cstorereq(
+                       'open-ils.cstore.direct.actor.usr_note.search.atomic', 
+                       { usr => $patronid, pub => 't' } );
+       }
+
+       $evt = $U->check_perms($reqr->id, $patron->home_ou, 'VIEW_USER');
+       return $evt if $evt;
+
+       return $U->cstorereq(
+               'open-ils.cstore.direct.actor.usr_note.search.atomic', { usr => $patronid } );
+}
+
+__PACKAGE__->register_method(
+    method    => 'create_user_note',
+    api_name  => 'open-ils.actor.note.create',
+    signature => q/
+               Creates a new note for the given user
+               @param authtoken The login session key
+               @param note The note object
+       /
+);
+sub create_user_note {
+       my( $self, $conn, $authtoken, $note ) = @_;
+       my $e = new_editor(xact=>1, authtoken=>$authtoken);
+       return $e->die_event unless $e->checkauth;
+
+       my $user = $e->retrieve_actor_user($note->usr)
+               or return $e->die_event;
+
+       return $e->die_event unless 
+               $e->allowed('UPDATE_USER',$user->home_ou);
+
+       $note->creator($e->requestor->id);
+       $e->create_actor_usr_note($note) or return $e->die_event;
+       $e->commit;
+       return $note->id;
+}
+
+
+__PACKAGE__->register_method(
+    method    => 'delete_user_note',
+    api_name  => 'open-ils.actor.note.delete',
+    signature => q/
+               Deletes a note for the given user
+               @param authtoken The login session key
+               @param noteid The note id
+       /
+);
+sub delete_user_note {
+       my( $self, $conn, $authtoken, $noteid ) = @_;
+
+       my $e = new_editor(xact=>1, authtoken=>$authtoken);
+       return $e->die_event unless $e->checkauth;
+       my $note = $e->retrieve_actor_usr_note($noteid)
+               or return $e->die_event;
+       my $user = $e->retrieve_actor_user($note->usr)
+               or return $e->die_event;
+       return $e->die_event unless 
+               $e->allowed('UPDATE_USER', $user->home_ou);
+       
+       $e->delete_actor_usr_note($note) or return $e->die_event;
+       $e->commit;
+       return 1;
+}
+
+
+__PACKAGE__->register_method(
+    method    => 'update_user_note',
+    api_name  => 'open-ils.actor.note.update',
+    signature => q/
+               @param authtoken The login session key
+               @param note The note
+       /
+);
+
+sub update_user_note {
+       my( $self, $conn, $auth, $note ) = @_;
+       my $e = new_editor(authtoken=>$auth, xact=>1);
+       return $e->die_event unless $e->checkauth;
+       my $patron = $e->retrieve_actor_user($note->usr)
+               or return $e->die_event;
+       return $e->die_event unless 
+               $e->allowed('UPDATE_USER', $patron->home_ou);
+       $e->update_actor_user_note($note)
+               or return $e->die_event;
+       $e->commit;
+       return 1;
+}
+
+__PACKAGE__->register_method(
+    method    => 'usrname_exists',
+    api_name  => 'open-ils.actor.username.exists',
+    signature => {
+        desc  => 'Check if a username is already taken (by an undeleted patron)',
+        param => [
+            {desc => 'Authentication token', type => 'string'},
+            {desc => 'Username',             type => 'string'}
+        ],
+        return => {
+            desc => 'id of existing user if username exists, undef otherwise.  Event on error'
+        },
+    }
+);
+
+sub usrname_exists {
+       my( $self, $conn, $auth, $usrname ) = @_;
+       my $e = new_editor(authtoken=>$auth);
+       return $e->event unless $e->checkauth;
+       my $a = $e->search_actor_user({usrname => $usrname}, {idlist=>1});
+       return $$a[0] if $a and @$a;
+       return undef;
+}
+
+# Retain the old typo API name just in case
+__PACKAGE__->register_method(
+    method   => 'fetch_org_by_shortname',
+    api_name => 'open-ils.actor.org_unit.retrieve_by_shorname',
+);
+__PACKAGE__->register_method(
+    method   => 'fetch_org_by_shortname',
+    api_name => 'open-ils.actor.org_unit.retrieve_by_shortname',
+);
+sub fetch_org_by_shortname {
+       my( $self, $conn, $sname ) = @_;
+       my $e = new_editor();
+       my $org = $e->search_actor_org_unit({ shortname => uc($sname)})->[0];
+       return $e->event unless $org;
+       return $org;
+}
+
+
+__PACKAGE__->register_method(
+    method   => 'session_home_lib',
+    api_name => 'open-ils.actor.session.home_lib',
+);
+
+sub session_home_lib {
+       my( $self, $conn, $auth ) = @_;
+       my $e = new_editor(authtoken=>$auth);
+       return undef unless $e->checkauth;
+       my $org = $e->retrieve_actor_org_unit($e->requestor->home_ou);
+       return $org->shortname;
+}
+
+__PACKAGE__->register_method(
+    method    => 'session_safe_token',
+    api_name  => 'open-ils.actor.session.safe_token',
+    signature => q/
+               Returns a hashed session ID that is safe for export to the world.
+               This safe token will expire after 1 hour of non-use.
+               @param auth Active authentication token
+       /
+);
+
+sub session_safe_token {
+       my( $self, $conn, $auth ) = @_;
+       my $e = new_editor(authtoken=>$auth);
+       return undef unless $e->checkauth;
+
+       my $safe_token = md5_hex($auth);
+
+       $cache ||= OpenSRF::Utils::Cache->new("global", 0);
+
+       # Add more like the following if needed...
+       $cache->put_cache(
+               "safe-token-home_lib-shortname-$safe_token",
+               $e->retrieve_actor_org_unit(
+                       $e->requestor->home_ou
+               )->shortname,
+               60 * 60
+       );
+
+       return $safe_token;
+}
+
+
+__PACKAGE__->register_method(
+    method    => 'safe_token_home_lib',
+    api_name  => 'open-ils.actor.safe_token.home_lib.shortname',
+    signature => q/
+               Returns the home library shortname from the session
+               asscociated with a safe token from generated by
+               open-ils.actor.session.safe_token.
+               @param safe_token Active safe token
+       /
+);
+
+sub safe_token_home_lib {
+       my( $self, $conn, $safe_token ) = @_;
+
+       $cache ||= OpenSRF::Utils::Cache->new("global", 0);
+       return $cache->get_cache( 'safe-token-home_lib-shortname-'. $safe_token );
+}
+
+
+__PACKAGE__->register_method(
+    method   => "user_retrieve_parts",
+    api_name => "open-ils.actor.user.retrieve.parts",
+);
+
+sub user_retrieve_parts {
+       my( $self, $client, $auth, $user_id, $fields ) = @_;
+       my $e = new_editor(authtoken => $auth);
+       return $e->event unless $e->checkauth;
+    $user_id ||= $e->requestor->id;
+       if( $e->requestor->id != $user_id ) {
+               return $e->event unless $e->allowed('VIEW_USER');
+       }
+       my @resp;
+       my $user = $e->retrieve_actor_user($user_id) or return $e->event;
+       push(@resp, $user->$_()) for(@$fields);
+       return \@resp;
+}
+
+
+__PACKAGE__->register_method (
+       method          => 'verify_user_password',
+       api_name        => 'open-ils.actor.verify_user_password',
+       signature       => q/
+        Given a barcode or username and the MD5 encoded password, 
+        returns 1 if the password is correct.  Returns 0 otherwise.
+       /
+);
+
+sub verify_user_password {
+    my($self, $conn, $auth, $username, $password) = @_;
+    my $e = new_editor(authtoken => $auth);
+       return $e->die_event unless $e->checkauth;
+
+    my $user = $e->search_actor_user({usrname => $username})->[0] or return 0;
+    return 0 if (!$user);
+    return $e->event unless $e->allowed('VIEW_USER', $user->home_ou);
+    return 1 if $user->passwd eq $password;
+    return 0;
+}
+
+sub check_password_strength_default {
+    my $password = shift;
+    # Use the default set of checks
+    if ( (length($password) < 7) or 
+            ($password !~ m/.*\d+.*/) or 
+            ($password !~ m/.*[A-Za-z]+.*/)
+       ) {
+        return 0;
+    }
+    return 1;
+}
+
+sub check_password_strength_custom {
+    my ($password, $pw_regex) = @_;
+
+    $pw_regex = qr/$pw_regex/;
+    if ($password !~  /$pw_regex/) {
+        return 0;
+    }
+    return 1;
+}
+
+
+1;
diff --git a/src/perl/lib/ShareStuff/AppUtils.pm b/src/perl/lib/ShareStuff/AppUtils.pm
new file mode 100644 (file)
index 0000000..796629d
--- /dev/null
@@ -0,0 +1,2110 @@
+package ShareStuff::AppUtils;
+# vim:noet:ts=4
+use strict; use warnings;
+use OpenSRF::Application;
+use base qw/OpenSRF::Application/;
+use OpenSRF::Utils::Cache;
+use OpenSRF::Utils::Logger qw/$logger/;
+use OpenSRF::EX qw(:try);
+use ShareStuff::Event;
+use ShareStuff::Const qw/:const/;
+use Data::Dumper;
+use ShareStuff::CStoreEditor;
+use Unicode::Normalize;
+use OpenSRF::Utils::SettingsClient;
+use UUID::Tiny;
+use Encode;
+use DateTime;
+use DateTime::Format::ISO8601;
+
+# ---------------------------------------------------------------------------
+# Pile of utilty methods used accross applications.
+# ---------------------------------------------------------------------------
+my $cache_client = "OpenSRF::Utils::Cache";
+
+
+# ---------------------------------------------------------------------------
+# on sucess, returns the created session, on failure throws ERROR exception
+# ---------------------------------------------------------------------------
+sub start_db_session {
+
+       my $self = shift;
+       my $session = OpenSRF::AppSession->connect( "open-ils.storage" );
+       my $trans_req = $session->request( "open-ils.storage.transaction.begin" );
+
+       my $trans_resp = $trans_req->recv();
+       if(ref($trans_resp) and UNIVERSAL::isa($trans_resp,"Error")) { throw $trans_resp; }
+       if( ! $trans_resp->content() ) {
+               throw OpenSRF::ERROR 
+                       ("Unable to Begin Transaction with database" );
+       }
+       $trans_req->finish();
+
+       $logger->debug("Setting global storage session to ".
+               "session: " . $session->session_id . " : " . $session->app );
+
+       return $session;
+}
+
+sub set_audit_info {
+       my $self = shift;
+       my $session = shift;
+       my $authtoken = shift;
+       my $user_id = shift;
+       my $ws_id = shift;
+       
+       my $audit_req = $session->request( "open-ils.storage.set_audit_info", $authtoken, $user_id, $ws_id );
+       my $audit_resp = $audit_req->recv();
+       $audit_req->finish();
+}
+
+my $PERM_QUERY = {
+    select => {
+        au => [ {
+            transform => 'permission.usr_has_perm',
+            alias => 'has_perm',
+            column => 'id',
+            params => []
+        } ]
+    },
+    from => 'au',
+    where => {},
+};
+
+
+# returns undef if user has all of the perms provided
+# returns the first failed perm on failure
+sub check_user_perms {
+       my($self, $user_id, $org_id, @perm_types ) = @_;
+       $logger->debug("Checking perms with user : $user_id , org: $org_id, @perm_types");
+
+       for my $type (@perm_types) {
+           $PERM_QUERY->{select}->{au}->[0]->{params} = [$type, $org_id];
+               $PERM_QUERY->{where}->{id} = $user_id;
+               return $type unless $self->is_true(OpenILS::Utils::CStoreEditor->new->json_query($PERM_QUERY)->[0]->{has_perm});
+       }
+       return undef;
+}
+
+# checks the list of user perms.  The first one that fails returns a new
+sub check_perms {
+       my( $self, $user_id, $org_id, @perm_types ) = @_;
+       my $t = $self->check_user_perms( $user_id, $org_id, @perm_types );
+       return OpenILS::Event->new('PERM_FAILURE', ilsperm => $t, ilspermloc => $org_id ) if $t;
+       return undef;
+}
+
+
+
+# ---------------------------------------------------------------------------
+# commits and destroys the session
+# ---------------------------------------------------------------------------
+sub commit_db_session {
+       my( $self, $session ) = @_;
+
+       my $req = $session->request( "open-ils.storage.transaction.commit" );
+       my $resp = $req->recv();
+
+       if(!$resp) {
+               throw OpenSRF::EX::ERROR ("Unable to commit db session");
+       }
+
+       if(UNIVERSAL::isa($resp,"Error")) { 
+               throw $resp ($resp->stringify); 
+       }
+
+       if(!$resp->content) {
+               throw OpenSRF::EX::ERROR ("Unable to commit db session");
+       }
+
+       $session->finish();
+       $session->disconnect();
+       $session->kill_me();
+}
+
+sub rollback_db_session {
+       my( $self, $session ) = @_;
+
+       my $req = $session->request("open-ils.storage.transaction.rollback");
+       my $resp = $req->recv();
+       if(UNIVERSAL::isa($resp,"Error")) { throw $resp;  }
+
+       $session->finish();
+       $session->disconnect();
+       $session->kill_me();
+}
+
+
+# returns undef it the event is not an ILS event
+# returns the event code otherwise
+sub event_code {
+       my( $self, $evt ) = @_;
+       return $evt->{ilsevent} if $self->is_event($evt);
+       return undef;
+}
+
+# some events, in particular auto-generated events, don't have an 
+# ilsevent key.  treat hashes with a 'textcode' key as events.
+sub is_event {
+       my ($self, $evt) = @_;
+       return (
+               ref($evt) eq 'HASH' and (
+                       defined $evt->{ilsevent} or
+                       defined $evt->{textcode}
+               )
+       );
+}
+
+# ---------------------------------------------------------------------------
+# Checks to see if a user is logged in.  Returns the user record on success,
+# throws an exception on error.
+# ---------------------------------------------------------------------------
+sub check_user_session {
+       my( $self, $user_session ) = @_;
+
+       my $content = $self->simplereq( 
+               'open-ils.auth', 
+               'open-ils.auth.session.retrieve', $user_session);
+
+    return undef if (!$content) or $self->event_code($content);
+       return $content;
+}
+
+# generic simple request returning a scalar value
+sub simplereq {
+       my($self, $service, $method, @params) = @_;
+       return $self->simple_scalar_request($service, $method, @params);
+}
+
+
+sub simple_scalar_request {
+       my($self, $service, $method, @params) = @_;
+
+       my $session = OpenSRF::AppSession->create( $service );
+
+       my $request = $session->request( $method, @params );
+
+       my $val;
+       my $err;
+       try  {
+
+               $val = $request->gather(1);     
+
+       } catch Error with {
+               $err = shift;
+       };
+
+       if( $err ) {
+               warn "received error : service=$service : method=$method : params=".Dumper(\@params) . "\n $err";
+               throw $err ("Call to $service for method $method \n failed with exception: $err : " );
+       }
+
+       return $val;
+}
+
+sub build_org_tree {
+       my( $self, $orglist ) = @_;
+
+       return $orglist unless ref $orglist; 
+    return $$orglist[0] if @$orglist == 1;
+
+       my @list = sort { 
+               $a->ou_type <=> $b->ou_type ||
+               $a->name cmp $b->name } @$orglist;
+
+       for my $org (@list) {
+
+               next unless ($org);
+        next if (!defined($org->parent_ou) || $org->parent_ou eq "");
+
+               my ($parent) = grep { $_->id == $org->parent_ou } @list;
+               next unless $parent;
+               $parent->children([]) unless defined($parent->children); 
+               push( @{$parent->children}, $org );
+       }
+
+       return $list[0];
+}
+
+sub fetch_closed_date {
+       my( $self, $cd ) = @_;
+       my $evt;
+       
+       $logger->debug("Fetching closed_date $cd from cstore");
+
+       my $cd_obj = $self->simplereq(
+               'open-ils.cstore',
+               'open-ils.cstore.direct.actor.org_unit.closed_date.retrieve', $cd );
+
+       if(!$cd_obj) {
+               $logger->info("closed_date $cd not found in the db");
+               $evt = OpenILS::Event->new('ACTOR_USER_NOT_FOUND');
+       }
+
+       return ($cd_obj, $evt);
+}
+
+sub fetch_user {
+       my( $self, $userid ) = @_;
+       my( $user, $evt );
+       
+       $logger->debug("Fetching user $userid from cstore");
+
+       $user = $self->simplereq(
+               'open-ils.cstore',
+               'open-ils.cstore.direct.actor.user.retrieve', $userid );
+
+       if(!$user) {
+               $logger->info("User $userid not found in the db");
+               $evt = OpenILS::Event->new('ACTOR_USER_NOT_FOUND');
+       }
+
+       return ($user, $evt);
+}
+
+sub checkses {
+       my( $self, $session ) = @_;
+       my $user = $self->check_user_session($session) or 
+        return (undef, OpenILS::Event->new('NO_SESSION'));
+    return ($user);
+}
+
+
+# verifiese the session and checks the permissions agains the
+# session user and the user's home_ou as the org id
+sub checksesperm {
+       my( $self, $session, @perms ) = @_;
+       my $user; my $evt; my $e; 
+       $logger->debug("Checking user session $session and perms @perms");
+       ($user, $evt) = $self->checkses($session);
+       return (undef, $evt) if $evt;
+       $evt = $self->check_perms($user->id, $user->home_ou, @perms);
+       return ($user, $evt);
+}
+
+
+sub checkrequestor {
+       my( $self, $staffobj, $userid, @perms ) = @_;
+       my $user; my $evt;
+       $userid = $staffobj->id unless defined $userid;
+
+       $logger->debug("checkrequestor(): requestor => " . $staffobj->id . ", target => $userid");
+
+       if( $userid ne $staffobj->id ) {
+               ($user, $evt) = $self->fetch_user($userid);
+               return (undef, $evt) if $evt;
+               $evt = $self->check_perms( $staffobj->id, $user->home_ou, @perms );
+
+       } else {
+               $user = $staffobj;
+       }
+
+       return ($user, $evt);
+}
+
+sub checkses_requestor {
+       my( $self, $authtoken, $targetid, @perms ) = @_;
+       my( $requestor, $target, $evt );
+
+       ($requestor, $evt) = $self->checkses($authtoken);
+       return (undef, undef, $evt) if $evt;
+
+       ($target, $evt) = $self->checkrequestor( $requestor, $targetid, @perms );
+       return( $requestor, $target, $evt);
+}
+
+sub fetch_copy {
+       my( $self, $copyid ) = @_;
+       my( $copy, $evt );
+
+       $logger->debug("Fetching copy $copyid from cstore");
+
+       $copy = $self->simplereq(
+               'open-ils.cstore',
+               'open-ils.cstore.direct.asset.copy.retrieve', $copyid );
+
+       if(!$copy) { $evt = OpenILS::Event->new('ASSET_COPY_NOT_FOUND'); }
+
+       return( $copy, $evt );
+}
+
+
+# retrieves a circ object by id
+sub fetch_circulation {
+       my( $self, $circid ) = @_;
+       my $circ; my $evt;
+       
+       $logger->debug("Fetching circ $circid from cstore");
+
+       $circ = $self->simplereq(
+               'open-ils.cstore',
+               "open-ils.cstore.direct.action.circulation.retrieve", $circid );
+
+       if(!$circ) {
+               $evt = OpenILS::Event->new('ACTION_CIRCULATION_NOT_FOUND', circid => $circid );
+       }
+
+       return ( $circ, $evt );
+}
+
+sub fetch_record_by_copy {
+       my( $self, $copyid ) = @_;
+       my( $record, $evt );
+
+       $logger->debug("Fetching record by copy $copyid from cstore");
+
+       $record = $self->simplereq(
+               'open-ils.cstore',
+               'open-ils.cstore.direct.asset.copy.retrieve', $copyid,
+               { flesh => 3,
+                 flesh_fields => {     bre => [ 'fixed_fields' ],
+                                       acn => [ 'record' ],
+                                       acp => [ 'call_number' ],
+                                 }
+               }
+       );
+
+       if(!$record) {
+               $evt = OpenILS::Event->new('BIBLIO_RECORD_ENTRY_NOT_FOUND');
+       } else {
+               $record = $record->call_number->record;
+       }
+
+       return ($record, $evt);
+}
+
+sub fetch_hold {
+       my( $self, $holdid ) = @_;
+       my( $hold, $evt );
+
+       $logger->debug("Fetching hold $holdid from cstore");
+
+       $hold = $self->simplereq(
+               'open-ils.cstore',
+               'open-ils.cstore.direct.action.hold_request.retrieve', $holdid);
+
+       $evt = OpenILS::Event->new('ACTION_HOLD_REQUEST_NOT_FOUND', holdid => $holdid) unless $hold;
+
+       return ($hold, $evt);
+}
+
+
+sub fetch_hold_transit_by_hold {
+       my( $self, $holdid ) = @_;
+       my( $transit, $evt );
+
+       $logger->debug("Fetching transit by hold $holdid from cstore");
+
+       $transit = $self->simplereq(
+               'open-ils.cstore',
+               'open-ils.cstore.direct.action.hold_transit_copy.search', { hold => $holdid } );
+
+       $evt = OpenILS::Event->new('ACTION_HOLD_TRANSIT_COPY_NOT_FOUND', holdid => $holdid) unless $transit;
+
+       return ($transit, $evt );
+}
+
+# fetches the captured, but not fulfilled hold attached to a given copy
+sub fetch_open_hold_by_copy {
+       my( $self, $copyid ) = @_;
+       $logger->debug("Searching for active hold for copy $copyid");
+       my( $hold, $evt );
+
+       $hold = $self->cstorereq(
+               'open-ils.cstore.direct.action.hold_request.search',
+               { 
+                       current_copy            => $copyid , 
+                       capture_time            => { "!=" => undef }, 
+                       fulfillment_time        => undef,
+                       cancel_time                     => undef,
+               } );
+
+       $evt = OpenILS::Event->new('ACTION_HOLD_REQUEST_NOT_FOUND', copyid => $copyid) unless $hold;
+       return ($hold, $evt);
+}
+
+sub fetch_hold_transit {
+       my( $self, $transid ) = @_;
+       my( $htransit, $evt );
+       $logger->debug("Fetching hold transit with hold id $transid");
+       $htransit = $self->cstorereq(
+               'open-ils.cstore.direct.action.hold_transit_copy.retrieve', $transid );
+       $evt = OpenILS::Event->new('ACTION_HOLD_TRANSIT_COPY_NOT_FOUND', id => $transid) unless $htransit;
+       return ($htransit, $evt);
+}
+
+sub fetch_copy_by_barcode {
+       my( $self, $barcode ) = @_;
+       my( $copy, $evt );
+
+       $logger->debug("Fetching copy by barcode $barcode from cstore");
+
+       $copy = $self->simplereq( 'open-ils.cstore',
+               'open-ils.cstore.direct.asset.copy.search', { barcode => $barcode, deleted => 'f'} );
+               #'open-ils.storage.direct.asset.copy.search.barcode', $barcode );
+
+       $evt = OpenILS::Event->new('ASSET_COPY_NOT_FOUND', barcode => $barcode) unless $copy;
+
+       return ($copy, $evt);
+}
+
+sub fetch_open_billable_transaction {
+       my( $self, $transid ) = @_;
+       my( $transaction, $evt );
+
+       $logger->debug("Fetching open billable transaction $transid from cstore");
+
+       $transaction = $self->simplereq(
+               'open-ils.cstore',
+               'open-ils.cstore.direct.money.open_billable_transaction_summary.retrieve',  $transid);
+
+       $evt = OpenILS::Event->new(
+               'MONEY_OPEN_BILLABLE_TRANSACTION_SUMMARY_NOT_FOUND', transid => $transid ) unless $transaction;
+
+       return ($transaction, $evt);
+}
+
+
+
+my %buckets;
+$buckets{'biblio'} = 'biblio_record_entry_bucket';
+$buckets{'callnumber'} = 'call_number_bucket';
+$buckets{'copy'} = 'copy_bucket';
+$buckets{'user'} = 'user_bucket';
+
+sub fetch_container {
+       my( $self, $id, $type ) = @_;
+       my( $bucket, $evt );
+
+       $logger->debug("Fetching container $id with type $type");
+
+       my $e = 'CONTAINER_CALL_NUMBER_BUCKET_NOT_FOUND';
+       $e = 'CONTAINER_BIBLIO_RECORD_ENTRY_BUCKET_NOT_FOUND' if $type eq 'biblio';
+       $e = 'CONTAINER_USER_BUCKET_NOT_FOUND' if $type eq 'user';
+       $e = 'CONTAINER_COPY_BUCKET_NOT_FOUND' if $type eq 'copy';
+
+       my $meth = $buckets{$type};
+       $bucket = $self->simplereq(
+               'open-ils.cstore',
+               "open-ils.cstore.direct.container.$meth.retrieve", $id );
+
+       $evt = OpenILS::Event->new(
+               $e, container => $id, container_type => $type ) unless $bucket;
+
+       return ($bucket, $evt);
+}
+
+
+sub fetch_container_e {
+       my( $self, $editor, $id, $type ) = @_;
+
+       my( $bucket, $evt );
+       $bucket = $editor->retrieve_container_copy_bucket($id) if $type eq 'copy';
+       $bucket = $editor->retrieve_container_call_number_bucket($id) if $type eq 'callnumber';
+       $bucket = $editor->retrieve_container_biblio_record_entry_bucket($id) if $type eq 'biblio';
+       $bucket = $editor->retrieve_container_user_bucket($id) if $type eq 'user';
+
+       $evt = $editor->event unless $bucket;
+       return ($bucket, $evt);
+}
+
+sub fetch_container_item_e {
+       my( $self, $editor, $id, $type ) = @_;
+
+       my( $bucket, $evt );
+       $bucket = $editor->retrieve_container_copy_bucket_item($id) if $type eq 'copy';
+       $bucket = $editor->retrieve_container_call_number_bucket_item($id) if $type eq 'callnumber';
+       $bucket = $editor->retrieve_container_biblio_record_entry_bucket_item($id) if $type eq 'biblio';
+       $bucket = $editor->retrieve_container_user_bucket_item($id) if $type eq 'user';
+
+       $evt = $editor->event unless $bucket;
+       return ($bucket, $evt);
+}
+
+
+
+
+
+sub fetch_container_item {
+       my( $self, $id, $type ) = @_;
+       my( $bucket, $evt );
+
+       $logger->debug("Fetching container item $id with type $type");
+
+       my $meth = $buckets{$type} . "_item";
+
+       $bucket = $self->simplereq(
+               'open-ils.cstore',
+               "open-ils.cstore.direct.container.$meth.retrieve", $id );
+
+
+       my $e = 'CONTAINER_CALL_NUMBER_BUCKET_ITEM_NOT_FOUND';
+       $e = 'CONTAINER_BIBLIO_RECORD_ENTRY_BUCKET_ITEM_NOT_FOUND' if $type eq 'biblio';
+       $e = 'CONTAINER_USER_BUCKET_ITEM_NOT_FOUND' if $type eq 'user';
+       $e = 'CONTAINER_COPY_BUCKET_ITEM_NOT_FOUND' if $type eq 'copy';
+
+       $evt = OpenILS::Event->new(
+               $e, itemid => $id, container_type => $type ) unless $bucket;
+
+       return ($bucket, $evt);
+}
+
+
+sub fetch_patron_standings {
+       my $self = shift;
+       $logger->debug("Fetching patron standings");    
+       return $self->simplereq(
+               'open-ils.cstore', 
+               'open-ils.cstore.direct.config.standing.search.atomic', { id => { '!=' => undef } });
+}
+
+
+sub fetch_permission_group_tree {
+       my $self = shift;
+       $logger->debug("Fetching patron profiles");     
+       return $self->simplereq(
+               'open-ils.actor', 
+               'open-ils.actor.groups.tree.retrieve' );
+}
+
+sub fetch_permission_group_descendants {
+    my( $self, $profile ) = @_;
+    my $group_tree = $self->fetch_permission_group_tree();
+    my $start_here;
+    my @groups;
+
+    # FIXME: okay, so it's not an org tree, but it is compatible
+    $self->walk_org_tree($group_tree, sub {
+        my $g = shift;
+        if ($g->id == $profile) {
+            $start_here = $g;
+        }
+    });
+
+    $self->walk_org_tree($start_here, sub {
+        my $g = shift;
+        push(@groups,$g->id);
+    });
+
+    return \@groups;
+}
+
+sub fetch_patron_circ_summary {
+       my( $self, $userid ) = @_;
+       $logger->debug("Fetching patron summary for $userid");
+       my $summary = $self->simplereq(
+               'open-ils.storage', 
+               "open-ils.storage.action.circulation.patron_summary", $userid );
+
+       if( $summary ) {
+               $summary->[0] ||= 0;
+               $summary->[1] ||= 0.0;
+               return $summary;
+       }
+       return undef;
+}
+
+
+sub fetch_copy_statuses {
+       my( $self ) = @_;
+       $logger->debug("Fetching copy statuses");
+       return $self->simplereq(
+               'open-ils.cstore', 
+               'open-ils.cstore.direct.config.copy_status.search.atomic', { id => { '!=' => undef } });
+}
+
+sub fetch_copy_location {
+       my( $self, $id ) = @_;
+       my $evt;
+       my $cl = $self->cstorereq(
+               'open-ils.cstore.direct.asset.copy_location.retrieve', $id );
+       $evt = OpenILS::Event->new('ASSET_COPY_LOCATION_NOT_FOUND') unless $cl;
+       return ($cl, $evt);
+}
+
+sub fetch_copy_locations {
+       my $self = shift; 
+       return $self->simplereq(
+               'open-ils.cstore', 
+               'open-ils.cstore.direct.asset.copy_location.search.atomic', { id => { '!=' => undef } });
+}
+
+sub fetch_copy_location_by_name {
+       my( $self, $name, $org ) = @_;
+       my $evt;
+       my $cl = $self->cstorereq(
+               'open-ils.cstore.direct.asset.copy_location.search',
+                       { name => $name, owning_lib => $org } );
+       $evt = OpenILS::Event->new('ASSET_COPY_LOCATION_NOT_FOUND') unless $cl;
+       return ($cl, $evt);
+}
+
+sub fetch_callnumber {
+       my( $self, $id, $flesh, $e ) = @_;
+
+       $e ||= OpenILS::Utils::CStoreEditor->new;
+
+       my $evt = OpenILS::Event->new( 'ASSET_CALL_NUMBER_NOT_FOUND', id => $id );
+       return( undef, $evt ) unless $id;
+
+       $logger->debug("Fetching callnumber $id");
+
+    my $cn = $e->retrieve_asset_call_number([
+        $id,
+        { flesh => $flesh, flesh_fields => { acn => [ 'prefix', 'suffix', 'label_class' ] } },
+    ]);
+
+       return ( $cn, $e->event );
+}
+
+my %ORG_CACHE; # - these rarely change, so cache them..
+sub fetch_org_unit {
+       my( $self, $id ) = @_;
+       return undef unless $id;
+       return $id if( ref($id) eq 'Fieldmapper::actor::org_unit' );
+       return $ORG_CACHE{$id} if $ORG_CACHE{$id};
+       $logger->debug("Fetching org unit $id");
+       my $evt = undef;
+
+       my $org = $self->simplereq(
+               'open-ils.cstore', 
+               'open-ils.cstore.direct.actor.org_unit.retrieve', $id );
+       $evt = OpenILS::Event->new( 'ACTOR_ORG_UNIT_NOT_FOUND', id => $id ) unless $org;
+       $ORG_CACHE{$id}  = $org;
+
+       return ($org, $evt);
+}
+
+sub fetch_stat_cat {
+       my( $self, $type, $id ) = @_;
+       my( $cat, $evt );
+       $logger->debug("Fetching $type stat cat: $id");
+       $cat = $self->simplereq(
+               'open-ils.cstore', 
+               "open-ils.cstore.direct.$type.stat_cat.retrieve", $id );
+
+       my $e = 'ASSET_STAT_CAT_NOT_FOUND';
+       $e = 'ACTOR_STAT_CAT_NOT_FOUND' if $type eq 'actor';
+
+       $evt = OpenILS::Event->new( $e, id => $id ) unless $cat;
+       return ( $cat, $evt );
+}
+
+sub fetch_stat_cat_entry {
+       my( $self, $type, $id ) = @_;
+       my( $entry, $evt );
+       $logger->debug("Fetching $type stat cat entry: $id");
+       $entry = $self->simplereq(
+               'open-ils.cstore', 
+               "open-ils.cstore.direct.$type.stat_cat_entry.retrieve", $id );
+
+       my $e = 'ASSET_STAT_CAT_ENTRY_NOT_FOUND';
+       $e = 'ACTOR_STAT_CAT_ENTRY_NOT_FOUND' if $type eq 'actor';
+
+       $evt = OpenILS::Event->new( $e, id => $id ) unless $entry;
+       return ( $entry, $evt );
+}
+
+sub fetch_stat_cat_entry_default {
+    my( $self, $type, $id ) = @_;
+    my( $entry_default, $evt );
+    $logger->debug("Fetching $type stat cat entry default: $id");
+    $entry_default = $self->simplereq(
+        'open-ils.cstore', 
+        "open-ils.cstore.direct.$type.stat_cat_entry_default.retrieve", $id );
+
+    my $e = 'ASSET_STAT_CAT_ENTRY_DEFAULT_NOT_FOUND';
+    $e = 'ACTOR_STAT_CAT_ENTRY_DEFAULT_NOT_FOUND' if $type eq 'actor';
+
+    $evt = OpenILS::Event->new( $e, id => $id ) unless $entry_default;
+    return ( $entry_default, $evt );
+}
+
+sub fetch_stat_cat_entry_default_by_stat_cat_and_org {
+    my( $self, $type, $stat_cat, $orgId ) = @_;
+    my $entry_default;
+    $logger->info("### Fetching $type stat cat entry default with stat_cat $stat_cat owned by org_unit $orgId");
+    $entry_default = $self->simplereq(
+        'open-ils.cstore', 
+        "open-ils.cstore.direct.$type.stat_cat_entry_default.search.atomic", 
+        { stat_cat => $stat_cat, owner => $orgId } );
+
+    $entry_default = $entry_default->[0];
+    return ($entry_default, undef) if $entry_default;
+
+    my $e = 'ASSET_STAT_CAT_ENTRY_DEFAULT_NOT_FOUND';
+    $e = 'ACTOR_STAT_CAT_ENTRY_DEFAULT_NOT_FOUND' if $type eq 'actor';
+    return (undef, OpenILS::Event->new($e) );
+}
+
+sub find_org {
+       my( $self, $org_tree, $orgid )  = @_;
+    return undef unless $org_tree and defined $orgid;
+       return $org_tree if ( $org_tree->id eq $orgid );
+       return undef unless ref($org_tree->children);
+       for my $c (@{$org_tree->children}) {
+               my $o = $self->find_org($c, $orgid);
+               return $o if $o;
+       }
+       return undef;
+}
+
+sub fetch_non_cat_type_by_name_and_org {
+       my( $self, $name, $orgId ) = @_;
+       $logger->debug("Fetching non cat type $name at org $orgId");
+       my $types = $self->simplereq(
+               'open-ils.cstore',
+               'open-ils.cstore.direct.config.non_cataloged_type.search.atomic',
+               { name => $name, owning_lib => $orgId } );
+       return ($types->[0], undef) if($types and @$types);
+       return (undef, OpenILS::Event->new('CONFIG_NON_CATALOGED_TYPE_NOT_FOUND') );
+}
+
+sub fetch_non_cat_type {
+       my( $self, $id ) = @_;
+       $logger->debug("Fetching non cat type $id");
+       my( $type, $evt );
+       $type = $self->simplereq(
+               'open-ils.cstore', 
+               'open-ils.cstore.direct.config.non_cataloged_type.retrieve', $id );
+       $evt = OpenILS::Event->new('CONFIG_NON_CATALOGED_TYPE_NOT_FOUND') unless $type;
+       return ($type, $evt);
+}
+
+sub DB_UPDATE_FAILED { 
+       my( $self, $payload ) = @_;
+       return OpenILS::Event->new('DATABASE_UPDATE_FAILED', 
+               payload => ($payload) ? $payload : undef ); 
+}
+
+sub fetch_booking_reservation {
+       my( $self, $id ) = @_;
+       my( $res, $evt );
+
+       $res = $self->simplereq(
+               'open-ils.cstore', 
+               'open-ils.cstore.direct.booking.reservation.retrieve', $id
+       );
+
+       # simplereq doesn't know how to flesh so ...
+       if ($res) {
+               $res->usr(
+                       $self->simplereq(
+                               'open-ils.cstore', 
+                               'open-ils.cstore.direct.actor.user.retrieve', $res->usr
+                       )
+               );
+
+               $res->target_resource_type(
+                       $self->simplereq(
+                               'open-ils.cstore', 
+                               'open-ils.cstore.direct.booking.resource_type.retrieve', $res->target_resource_type
+                       )
+               );
+
+               if ($res->current_resource) {
+                       $res->current_resource(
+                               $self->simplereq(
+                                       'open-ils.cstore', 
+                                       'open-ils.cstore.direct.booking.resource.retrieve', $res->current_resource
+                               )
+                       );
+
+                       if ($self->is_true( $res->target_resource_type->catalog_item )) {
+                               $res->current_resource->catalog_item( $self->fetch_copy_by_barcode( $res->current_resource->barcode ) );
+                       }
+               }
+
+               if ($res->target_resource) {
+                       $res->target_resource(
+                               $self->simplereq(
+                                       'open-ils.cstore', 
+                                       'open-ils.cstore.direct.booking.resource.retrieve', $res->target_resource
+                               )
+                       );
+
+                       if ($self->is_true( $res->target_resource_type->catalog_item )) {
+                               $res->target_resource->catalog_item( $self->fetch_copy_by_barcode( $res->target_resource->barcode ) );
+                       }
+               }
+
+       } else {
+               $evt = OpenILS::Event->new('RESERVATION_NOT_FOUND');
+       }
+
+       return ($res, $evt);
+}
+
+sub fetch_circ_duration_by_name {
+       my( $self, $name ) = @_;
+       my( $dur, $evt );
+       $dur = $self->simplereq(
+               'open-ils.cstore', 
+               'open-ils.cstore.direct.config.rules.circ_duration.search.atomic', { name => $name } );
+       $dur = $dur->[0];
+       $evt = OpenILS::Event->new('CONFIG_RULES_CIRC_DURATION_NOT_FOUND') unless $dur;
+       return ($dur, $evt);
+}
+
+sub fetch_recurring_fine_by_name {
+       my( $self, $name ) = @_;
+       my( $obj, $evt );
+       $obj = $self->simplereq(
+               'open-ils.cstore', 
+               'open-ils.cstore.direct.config.rules.recurring_fine.search.atomic', { name => $name } );
+       $obj = $obj->[0];
+       $evt = OpenILS::Event->new('CONFIG_RULES_RECURRING_FINE_NOT_FOUND') unless $obj;
+       return ($obj, $evt);
+}
+
+sub fetch_max_fine_by_name {
+       my( $self, $name ) = @_;
+       my( $obj, $evt );
+       $obj = $self->simplereq(
+               'open-ils.cstore', 
+               'open-ils.cstore.direct.config.rules.max_fine.search.atomic', { name => $name } );
+       $obj = $obj->[0];
+       $evt = OpenILS::Event->new('CONFIG_RULES_MAX_FINE_NOT_FOUND') unless $obj;
+       return ($obj, $evt);
+}
+
+sub fetch_hard_due_date_by_name {
+       my( $self, $name ) = @_;
+       my( $obj, $evt );
+       $obj = $self->simplereq(
+               'open-ils.cstore', 
+               'open-ils.cstore.direct.config.hard_due_date.search.atomic', { name => $name } );
+       $obj = $obj->[0];
+       $evt = OpenILS::Event->new('CONFIG_RULES_HARD_DUE_DATE_NOT_FOUND') unless $obj;
+       return ($obj, $evt);
+}
+
+sub storagereq {
+       my( $self, $method, @params ) = @_;
+       return $self->simplereq(
+               'open-ils.storage', $method, @params );
+}
+
+sub storagereq_xact {
+       my($self, $method, @params) = @_;
+       my $ses = $self->start_db_session();
+       my $val = $ses->request($method, @params)->gather(1);
+       $self->rollback_db_session($ses);
+    return $val;
+}
+
+sub cstorereq {
+       my( $self, $method, @params ) = @_;
+       return $self->simplereq(
+               'open-ils.cstore', $method, @params );
+}
+
+sub event_equals {
+       my( $self, $e, $name ) =  @_;
+       if( $e and ref($e) eq 'HASH' and 
+               defined($e->{textcode}) and $e->{textcode} eq $name ) {
+               return 1 ;
+       }
+       return 0;
+}
+
+sub logmark {
+       my( undef, $f, $l ) = caller(0);
+       my( undef, undef, undef, $s ) = caller(1);
+       $s =~ s/.*:://g;
+       $f =~ s/.*\///g;
+       $logger->debug("LOGMARK: $f:$l:$s");
+}
+
+# takes a copy id 
+sub fetch_open_circulation {
+       my( $self, $cid ) = @_;
+       $self->logmark;
+
+       my $e = OpenILS::Utils::CStoreEditor->new;
+    my $circ = $e->search_action_circulation({
+        target_copy => $cid, 
+        stop_fines_time => undef, 
+        checkin_time => undef
+    })->[0];
+    
+    return ($circ, $e->event);
+}
+
+my $copy_statuses;
+sub copy_status_from_name {
+       my( $self, $name ) = @_;
+       $copy_statuses = $self->fetch_copy_statuses unless $copy_statuses;
+       for my $status (@$copy_statuses) { 
+               return $status if( $status->name =~ /$name/i );
+       }
+       return undef;
+}
+
+sub copy_status_to_name {
+       my( $self, $sid ) = @_;
+       $copy_statuses = $self->fetch_copy_statuses unless $copy_statuses;
+       for my $status (@$copy_statuses) { 
+               return $status->name if( $status->id == $sid );
+       }
+       return undef;
+}
+
+
+sub copy_status {
+       my( $self, $arg ) = @_;
+       return $arg if ref $arg;
+       $copy_statuses = $self->fetch_copy_statuses unless $copy_statuses;
+       my ($stat) = grep { $_->id == $arg } @$copy_statuses;
+       return $stat;
+}
+
+sub fetch_open_transit_by_copy {
+       my( $self, $copyid ) = @_;
+       my($transit, $evt);
+       $transit = $self->cstorereq(
+               'open-ils.cstore.direct.action.transit_copy.search',
+               { target_copy => $copyid, dest_recv_time => undef });
+       $evt = OpenILS::Event->new('ACTION_TRANSIT_COPY_NOT_FOUND') unless $transit;
+       return ($transit, $evt);
+}
+
+sub unflesh_copy {
+       my( $self, $copy ) = @_;
+       return undef unless $copy;
+       $copy->status( $copy->status->id ) if ref($copy->status);
+       $copy->location( $copy->location->id ) if ref($copy->location);
+       $copy->circ_lib( $copy->circ_lib->id ) if ref($copy->circ_lib);
+       return $copy;
+}
+
+sub unflesh_reservation {
+       my( $self, $reservation ) = @_;
+       return undef unless $reservation;
+       $reservation->usr( $reservation->usr->id ) if ref($reservation->usr);
+       $reservation->target_resource_type( $reservation->target_resource_type->id ) if ref($reservation->target_resource_type);
+       $reservation->target_resource( $reservation->target_resource->id ) if ref($reservation->target_resource);
+       $reservation->current_resource( $reservation->current_resource->id ) if ref($reservation->current_resource);
+       return $reservation;
+}
+
+# un-fleshes a copy and updates it in the DB
+# returns a DB_UPDATE_FAILED event on error
+# returns undef on success
+sub update_copy {
+       my( $self, %params ) = @_;
+
+       my $copy                = $params{copy} || die "update_copy(): copy required";
+       my $editor      = $params{editor} || die "update_copy(): copy editor required";
+       my $session = $params{session};
+
+       $logger->debug("Updating copy in the database: " . $copy->id);
+
+       $self->unflesh_copy($copy);
+       $copy->editor( $editor );
+       $copy->edit_date( 'now' );
+
+       my $s;
+       my $meth = 'open-ils.storage.direct.asset.copy.update';
+
+       $s = $session->request( $meth, $copy )->gather(1) if $session;
+       $s = $self->storagereq( $meth, $copy ) unless $session;
+
+       $logger->debug("Update of copy ".$copy->id." returned: $s");
+
+       return $self->DB_UPDATE_FAILED($copy) unless $s;
+       return undef;
+}
+
+sub update_reservation {
+       my( $self, %params ) = @_;
+
+       my $reservation = $params{reservation}  || die "update_reservation(): reservation required";
+       my $editor              = $params{editor} || die "update_reservation(): copy editor required";
+       my $session             = $params{session};
+
+       $logger->debug("Updating copy in the database: " . $reservation->id);
+
+       $self->unflesh_reservation($reservation);
+
+       my $s;
+       my $meth = 'open-ils.cstore.direct.booking.reservation.update';
+
+       $s = $session->request( $meth, $reservation )->gather(1) if $session;
+       $s = $self->cstorereq( $meth, $reservation ) unless $session;
+
+       $logger->debug("Update of copy ".$reservation->id." returned: $s");
+
+       return $self->DB_UPDATE_FAILED($reservation) unless $s;
+       return undef;
+}
+
+sub fetch_billable_xact {
+       my( $self, $id ) = @_;
+       my($xact, $evt);
+       $logger->debug("Fetching billable transaction %id");
+       $xact = $self->cstorereq(
+               'open-ils.cstore.direct.money.billable_transaction.retrieve', $id );
+       $evt = OpenILS::Event->new('MONEY_BILLABLE_TRANSACTION_NOT_FOUND') unless $xact;
+       return ($xact, $evt);
+}
+
+sub fetch_billable_xact_summary {
+       my( $self, $id ) = @_;
+       my($xact, $evt);
+       $logger->debug("Fetching billable transaction summary %id");
+       $xact = $self->cstorereq(
+               'open-ils.cstore.direct.money.billable_transaction_summary.retrieve', $id );
+       $evt = OpenILS::Event->new('MONEY_BILLABLE_TRANSACTION_NOT_FOUND') unless $xact;
+       return ($xact, $evt);
+}
+
+sub fetch_fleshed_copy {
+       my( $self, $id ) = @_;
+       my( $copy, $evt );
+       $logger->info("Fetching fleshed copy $id");
+       $copy = $self->cstorereq(
+               "open-ils.cstore.direct.asset.copy.retrieve", $id,
+               { flesh => 1,
+                 flesh_fields => { acp => [ qw/ circ_lib location status stat_cat_entries / ] }
+               }
+       );
+       $evt = OpenILS::Event->new('ASSET_COPY_NOT_FOUND', id => $id) unless $copy;
+       return ($copy, $evt);
+}
+
+
+# returns the org that owns the callnumber that the copy
+# is attached to
+sub fetch_copy_owner {
+       my( $self, $copyid ) = @_;
+       my( $copy, $cn, $evt );
+       $logger->debug("Fetching copy owner $copyid");
+       ($copy, $evt) = $self->fetch_copy($copyid);
+       return (undef,$evt) if $evt;
+       ($cn, $evt) = $self->fetch_callnumber($copy->call_number);
+       return (undef,$evt) if $evt;
+       return ($cn->owning_lib);
+}
+
+sub fetch_copy_note {
+       my( $self, $id ) = @_;
+       my( $note, $evt );
+       $logger->debug("Fetching copy note $id");
+       $note = $self->cstorereq(
+               'open-ils.cstore.direct.asset.copy_note.retrieve', $id );
+       $evt = OpenILS::Event->new('ASSET_COPY_NOTE_NOT_FOUND', id => $id ) unless $note;
+       return ($note, $evt);
+}
+
+sub fetch_call_numbers_by_title {
+       my( $self, $titleid ) = @_;
+       $logger->info("Fetching call numbers by title $titleid");
+       return $self->cstorereq(
+               'open-ils.cstore.direct.asset.call_number.search.atomic', 
+               { record => $titleid, deleted => 'f' });
+               #'open-ils.storage.direct.asset.call_number.search.record.atomic', $titleid);
+}
+
+sub fetch_copies_by_call_number {
+       my( $self, $cnid ) = @_;
+       $logger->info("Fetching copies by call number $cnid");
+       return $self->cstorereq(
+               'open-ils.cstore.direct.asset.copy.search.atomic', { call_number => $cnid, deleted => 'f' } );
+               #'open-ils.storage.direct.asset.copy.search.call_number.atomic', $cnid );
+}
+
+sub fetch_user_by_barcode {
+       my( $self, $bc ) = @_;
+       my $cardid = $self->cstorereq(
+               'open-ils.cstore.direct.actor.card.id_list', { barcode => $bc } );
+       return (undef, OpenILS::Event->new('ACTOR_CARD_NOT_FOUND', barcode => $bc)) unless $cardid;
+       my $user = $self->cstorereq(
+               'open-ils.cstore.direct.actor.user.search', { card => $cardid } );
+       return (undef, OpenILS::Event->new('ACTOR_USER_NOT_FOUND', card => $cardid)) unless $user;
+       return ($user);
+       
+}
+
+sub fetch_bill {
+       my( $self, $billid ) = @_;
+       $logger->debug("Fetching billing $billid");
+       my $bill = $self->cstorereq(
+               'open-ils.cstore.direct.money.billing.retrieve', $billid );
+       my $evt = OpenILS::Event->new('MONEY_BILLING_NOT_FOUND') unless $bill;
+       return($bill, $evt);
+}
+
+sub walk_org_tree {
+       my( $self, $node, $callback ) = @_;
+       return unless $node;
+       $callback->($node);
+       if( $node->children ) {
+               $self->walk_org_tree($_, $callback) for @{$node->children};
+       }
+}
+
+sub is_true {
+       my( $self, $item ) = @_;
+       return 1 if $item and $item !~ /^f$/i;
+       return 0;
+}
+
+
+sub patientreq {
+    my ($self, $client, $service, $method, @params) = @_;
+    my ($response, $err);
+
+    my $session = create OpenSRF::AppSession($service);
+    my $request = $session->request($method, @params);
+
+    my $spurt = 10;
+    my $give_up = time + 1000;
+
+    try {
+        while (time < $give_up) {
+            $response = $request->recv("timeout" => $spurt);
+            last if $request->complete;
+
+            $client->status(new OpenSRF::DomainObject::oilsContinueStatus);
+        }
+    } catch Error with {
+        $err = shift;
+    };
+
+    if ($err) {
+        warn "received error : service=$service : method=$method : params=".Dumper(\@params) . "\n $err";
+        throw $err ("Call to $service for method $method \n failed with exception: $err : " );
+    }
+
+    return $response->content;
+}
+
+# This logic now lives in storage
+sub __patron_money_owed {
+       my( $self, $patronid ) = @_;
+       my $ses = OpenSRF::AppSession->create('open-ils.storage');
+       my $req = $ses->request(
+               'open-ils.storage.money.billable_transaction.summary.search',
+               { usr => $patronid, xact_finish => undef } );
+
+       my $total = 0;
+       my $data;
+       while( $data = $req->recv ) {
+               $data = $data->content;
+               $total += $data->balance_owed;
+       }
+       return $total;
+}
+
+sub patron_money_owed {
+       my( $self, $userid ) = @_;
+       my $ses = $self->start_db_session();
+       my $val = $ses->request(
+               'open-ils.storage.actor.user.total_owed', $userid)->gather(1);
+       $self->rollback_db_session($ses);
+       return $val;
+}
+
+sub patron_total_items_out {
+       my( $self, $userid ) = @_;
+       my $ses = $self->start_db_session();
+       my $val = $ses->request(
+               'open-ils.storage.actor.user.total_out', $userid)->gather(1);
+       $self->rollback_db_session($ses);
+       return $val;
+}
+
+
+
+
+#---------------------------------------------------------------------
+# Returns  ($summary, $event) 
+#---------------------------------------------------------------------
+sub fetch_mbts {
+       my $self = shift;
+       my $id  = shift;
+       my $e = shift || OpenILS::Utils::CStoreEditor->new;
+       $id = $id->id if ref($id);
+    
+    my $xact = $e->retrieve_money_billable_transaction_summary($id)
+           or return (undef, $e->event);
+
+    return ($xact);
+}
+
+
+#---------------------------------------------------------------------
+# Given a list of money.billable_transaction objects, this creates
+# transaction summary objects for each
+#--------------------------------------------------------------------
+sub make_mbts {
+       my $self = shift;
+    my $e = shift;
+       my @xacts = @_;
+       return () if (!@xacts);
+    return @{$e->search_money_billable_transaction_summary({id => [ map { $_->id } @xacts ]})};
+}
+               
+               
+sub ou_ancestor_setting_value {
+    my($self, $org_id, $name, $e) = @_;
+    $e = $e || OpenILS::Utils::CStoreEditor->new;
+    my $set = $self->ou_ancestor_setting($org_id, $name, $e);
+    return $set->{value} if $set;
+    return undef;
+}
+
+
+# If an authentication token is provided AND this org unit setting has a
+# view_perm, then make sure the user referenced by the auth token has
+# that permission.  This means that if you call this method without an
+# authtoken param, you can get whatever org unit setting values you want.
+# API users beware.
+#
+# NOTE: If you supply an editor ($e) arg AND an auth token arg, the editor's
+# authtoken is checked, but the $auth arg is NOT checked.  To say that another
+# way, be sure NOT to pass an editor argument if you want your token checked.
+# Otherwise the auth arg is just a flag saying "check the editor".  
+
+sub ou_ancestor_setting {
+    my( $self, $orgid, $name, $e, $auth ) = @_;
+    $e = $e || OpenILS::Utils::CStoreEditor->new(
+        (defined $auth) ? (authtoken => $auth) : ()
+    );
+    my $coust = $e->retrieve_config_org_unit_setting_type([
+        $name, {flesh => 1, flesh_fields => {coust => ['view_perm']}}
+    ]);
+
+    if ($auth && $coust && $coust->view_perm) {
+        # And you can't have permission if you don't have a valid session.
+        return undef if not $e->checkauth;
+        # And now that we know you MIGHT have permission, we check it.
+        return undef if not $e->allowed($coust->view_perm->code, $orgid);
+    }
+
+    my $query = {from => ['actor.org_unit_ancestor_setting', $name, $orgid]};
+    my $setting = $e->json_query($query)->[0];
+    return undef unless $setting;
+    return {org => $setting->{org_unit}, value => OpenSRF::Utils::JSON->JSON2perl($setting->{value})};
+}      
+               
+
+# returns the ISO8601 string representation of the requested epoch in GMT
+sub epoch2ISO8601 {
+    my( $self, $epoch ) = @_;
+    my ($sec,$min,$hour,$mday,$mon,$year) = gmtime($epoch);
+    $year += 1900; $mon += 1;
+    my $date = sprintf(
+        '%s-%0.2d-%0.2dT%0.2d:%0.2d:%0.2d-00',
+        $year, $mon, $mday, $hour, $min, $sec);
+    return $date;
+}
+                       
+sub find_highest_perm_org {
+       my ( $self, $perm, $userid, $start_org, $org_tree ) = @_;
+       my $org = $self->find_org($org_tree, $start_org );
+
+       my $lastid = -1;
+       while( $org ) {
+               last if ($self->check_perms( $userid, $org->id, $perm )); # perm failed
+               $lastid = $org->id;
+               $org = $self->find_org( $org_tree, $org->parent_ou() );
+       }
+
+       return $lastid;
+}
+
+
+# returns the org_unit ID's 
+sub user_has_work_perm_at {
+    my($self, $e, $perm, $options, $user_id) = @_;
+    $options ||= {};
+    $user_id = (defined $user_id) ? $user_id : $e->requestor->id;
+
+    my $func = 'permission.usr_has_perm_at';
+    $func = $func.'_all' if $$options{descendants};
+
+    my $orgs = $e->json_query({from => [$func, $user_id, $perm]});
+    $orgs = [map { $_->{ (keys %$_)[0] } } @$orgs];
+
+    return $orgs unless $$options{objects};
+
+    return $e->search_actor_org_unit({id => $orgs});
+}
+
+sub get_user_work_ou_ids {
+    my($self, $e, $userid) = @_;
+    my $work_orgs = $e->json_query({
+        select => {puwoum => ['work_ou']},
+        from => 'puwoum',
+        where => {usr => $e->requestor->id}});
+
+    return [] unless @$work_orgs;
+    my @work_orgs;
+    push(@work_orgs, $_->{work_ou}) for @$work_orgs;
+
+    return \@work_orgs;
+}
+
+
+my $org_types;
+sub get_org_types {
+       my($self, $client) = @_;
+       return $org_types if $org_types;
+       return $org_types = OpenILS::Utils::CStoreEditor->new->retrieve_all_actor_org_unit_type();
+}
+
+my %ORG_TREE;
+sub get_org_tree {
+       my $self = shift;
+       my $locale = shift || '';
+       my $cache = OpenSRF::Utils::Cache->new("global", 0);
+       my $tree = $ORG_TREE{$locale} || $cache->get_cache("orgtree.$locale");
+       return $tree if $tree;
+
+       my $ses = OpenILS::Utils::CStoreEditor->new;
+       $ses->session->session_locale($locale);
+       $tree = $ses->search_actor_org_unit( 
+               [
+                       {"parent_ou" => undef },
+                       {
+                               flesh                           => -1,
+                               flesh_fields    => { aou =>  ['children'] },
+                               order_by                        => { aou => 'name'}
+                       }
+               ]
+       )->[0];
+
+    $ORG_TREE{$locale} = $tree;
+       $cache->put_cache("orgtree.$locale", $tree);
+       return $tree;
+}
+
+sub get_org_descendants {
+       my($self, $org_id, $depth) = @_;
+
+       my $select = {
+               transform => 'actor.org_unit_descendants',
+               column => 'id',
+               result_field => 'id',
+       };
+       $select->{params} = [$depth] if defined $depth;
+
+       my $org_list = OpenILS::Utils::CStoreEditor->new->json_query({
+               select => {aou => [$select]},
+        from => 'aou',
+               where => {id => $org_id}
+       });
+       my @orgs;
+       push(@orgs, $_->{id}) for @$org_list;
+       return \@orgs;
+}
+
+sub get_org_ancestors {
+       my($self, $org_id, $use_cache) = @_;
+
+    my ($cache, $orgs);
+
+    if ($use_cache) {
+        $cache = OpenSRF::Utils::Cache->new("global", 0);
+        $orgs = $cache->get_cache("org.ancestors.$org_id");
+        return $orgs if $orgs;
+    }
+
+       my $org_list = OpenILS::Utils::CStoreEditor->new->json_query({
+               select => {
+                       aou => [{
+                               transform => 'actor.org_unit_ancestors',
+                               column => 'id',
+                               result_field => 'id',
+                               params => []
+                       }],
+               },
+               from => 'aou',
+               where => {id => $org_id}
+       });
+
+       $orgs = [ map { $_->{id} } @$org_list ];
+
+    $cache->put_cache("org.ancestors.$org_id", $orgs) if $use_cache;
+       return $orgs;
+}
+
+sub get_org_full_path {
+       my($self, $org_id, $depth) = @_;
+
+    my $query = {
+        select => {
+                       aou => [{
+                               transform => 'actor.org_unit_full_path',
+                               column => 'id',
+                               result_field => 'id',
+                       }],
+               },
+               from => 'aou',
+               where => {id => $org_id}
+       };
+
+    $query->{select}->{aou}->[0]->{params} = [$depth] if defined $depth;
+       my $org_list = OpenILS::Utils::CStoreEditor->new->json_query($query);
+    return [ map {$_->{id}} @$org_list ];
+}
+
+# returns the ID of the org unit ancestor at the specified depth
+sub org_unit_ancestor_at_depth {
+    my($class, $org_id, $depth) = @_;
+    my $resp = OpenILS::Utils::CStoreEditor->new->json_query(
+        {from => ['actor.org_unit_ancestor_at_depth', $org_id, $depth]})->[0];
+    return ($resp) ? $resp->{id} : undef;
+}
+
+# returns the user's configured locale as a string.  Defaults to en-US if none is configured.
+sub get_user_locale {
+       my($self, $user_id, $e) = @_;
+       $e ||= OpenILS::Utils::CStoreEditor->new;
+
+       # first, see if the user has an explicit locale set
+       my $setting = $e->search_actor_user_setting(
+               {usr => $user_id, name => 'global.locale'})->[0];
+       return OpenSRF::Utils::JSON->JSON2perl($setting->value) if $setting;
+
+       my $user = $e->retrieve_actor_user($user_id) or return $e->event;
+       return $self->get_org_locale($user->home_ou, $e);
+}
+
+# returns org locale setting
+sub get_org_locale {
+       my($self, $org_id, $e) = @_;
+       $e ||= OpenILS::Utils::CStoreEditor->new;
+
+       my $locale;
+       if(defined $org_id) {
+               $locale = $self->ou_ancestor_setting_value($org_id, 'global.default_locale', $e);
+               return $locale if $locale;
+       }
+
+       # system-wide default
+       my $sclient = OpenSRF::Utils::SettingsClient->new;
+       $locale = $sclient->config_value('default_locale');
+    return $locale if $locale;
+
+       # if nothing else, fallback to locale=cowboy
+       return 'en-US';
+}
+
+
+# xml-escape non-ascii characters
+sub entityize { 
+    my($self, $string, $form) = @_;
+       $form ||= "";
+
+       # If we're going to convert non-ASCII characters to XML entities,
+       # we had better be dealing with a UTF8 string to begin with
+       $string = decode_utf8($string);
+
+       if ($form eq 'D') {
+               $string = NFD($string);
+       } else {
+               $string = NFC($string);
+       }
+
+       # Convert raw ampersands to entities
+       $string =~ s/&(?!\S+;)/&amp;/gso;
+
+       # Convert Unicode characters to entities
+       $string =~ s/([\x{0080}-\x{fffd}])/sprintf('&#x%X;',ord($1))/sgoe;
+
+       return $string;
+}
+
+# x0000-x0008 isn't legal in XML documents
+# XXX Perhaps this should just go into our standard entityize method
+sub strip_ctrl_chars {
+       my ($self, $string) = @_;
+
+       $string =~ s/([\x{0000}-\x{0008}])//sgoe; 
+       return $string;
+}
+
+sub get_copy_price {
+       my($self, $e, $copy, $volume) = @_;
+
+       $copy->price(0) if $copy->price and $copy->price < 0;
+
+       return $copy->price if $copy->price and $copy->price > 0;
+
+
+       my $owner;
+       if(ref $volume) {
+               if($volume->id == OILS_PRECAT_CALL_NUMBER) {
+                       $owner = $copy->circ_lib;
+               } else {
+                       $owner = $volume->owning_lib;
+               }
+       } else {
+               if($copy->call_number == OILS_PRECAT_CALL_NUMBER) {
+                       $owner = $copy->circ_lib;
+               } else {
+                       $owner = $e->retrieve_asset_call_number($copy->call_number)->owning_lib;
+               }
+       }
+
+       my $default_price = $self->ou_ancestor_setting_value(
+               $owner, OILS_SETTING_DEF_ITEM_PRICE, $e) || 0;
+
+       return $default_price unless defined $copy->price;
+
+       # price is 0.  Use the default?
+    my $charge_on_0 = $self->ou_ancestor_setting_value(
+        $owner, OILS_SETTING_CHARGE_LOST_ON_ZERO, $e) || 0;
+
+       return $default_price if $charge_on_0;
+       return 0;
+}
+
+# given a transaction ID, this returns the context org_unit for the transaction
+sub xact_org {
+    my($self, $xact_id, $e) = @_;
+    $e ||= OpenILS::Utils::CStoreEditor->new;
+    
+    my $loc = $e->json_query({
+        "select" => {circ => ["circ_lib"]},
+        from     => "circ",
+        "where"  => {id => $xact_id},
+    });
+
+    return $loc->[0]->{circ_lib} if @$loc;
+
+    $loc = $e->json_query({
+        "select" => {bresv => ["request_lib"]},
+        from     => "bresv",
+        "where"  => {id => $xact_id},
+    });
+
+    return $loc->[0]->{request_lib} if @$loc;
+
+    $loc = $e->json_query({
+        "select" => {mg => ["billing_location"]},
+        from     => "mg",
+        "where"  => {id => $xact_id},
+    });
+
+    return $loc->[0]->{billing_location};
+}
+
+
+sub find_event_def_by_hook {
+    my($self, $hook, $context_org, $e) = @_;
+
+    $e ||= OpenILS::Utils::CStoreEditor->new;
+
+    my $orgs = $self->get_org_ancestors($context_org);
+
+    # search from the context org up
+    for my $org_id (reverse @$orgs) {
+
+        my $def = $e->search_action_trigger_event_definition(
+            {hook => $hook, owner => $org_id})->[0];
+
+        return $def if $def;
+    }
+
+    return undef;
+}
+
+
+
+# If an event_def ID is not provided, use the hook and context org to find the 
+# most appropriate event.  create the event, fire it, then return the resulting
+# event with fleshed template_output and error_output
+sub fire_object_event {
+    my($self, $event_def, $hook, $object, $context_org, $granularity, $user_data, $client) = @_;
+
+    my $e = OpenILS::Utils::CStoreEditor->new;
+    my $def;
+
+    my $auto_method = "open-ils.trigger.event.autocreate.by_definition";
+
+    if($event_def) {
+        $def = $e->retrieve_action_trigger_event_definition($event_def)
+            or return $e->event;
+
+        $auto_method .= '.include_inactive';
+
+    } else {
+
+        # find the most appropriate event def depending on context org
+        $def = $self->find_event_def_by_hook($hook, $context_org, $e) 
+            or return $e->event;
+    }
+
+    my $final_resp;
+
+    if($def->group_field) {
+        # we have a list of objects
+        $object = [$object] unless ref $object eq 'ARRAY';
+
+        my @event_ids;
+        $user_data ||= [];
+        for my $i (0..$#$object) {
+            my $obj = $$object[$i];
+            my $udata = $$user_data[$i];
+            my $event_id = $self->simplereq(
+                'open-ils.trigger', $auto_method, $def->id, $obj, $context_org, $udata);
+            push(@event_ids, $event_id);
+        }
+
+        $logger->info("EVENTS = " . OpenSRF::Utils::JSON->perl2JSON(\@event_ids));
+
+        my $resp;
+        if (not defined $client) {
+            $resp = $self->simplereq(
+                'open-ils.trigger',
+                'open-ils.trigger.event_group.fire',
+                \@event_ids);
+        } else {
+            $resp = $self->patientreq(
+                $client,
+                "open-ils.trigger", "open-ils.trigger.event_group.fire",
+                \@event_ids
+            );
+        }
+
+        if($resp and $resp->{events} and @{$resp->{events}}) {
+
+            $e->xact_begin;
+            $final_resp = $e->retrieve_action_trigger_event([
+                $resp->{events}->[0]->id,
+                {flesh => 1, flesh_fields => {atev => ['template_output', 'error_output']}}
+            ]);
+            $e->rollback;
+        }
+
+    } else {
+
+        $object = $$object[0] if ref $object eq 'ARRAY';
+
+        my $event_id;
+        my $resp;
+
+        if (not defined $client) {
+            $event_id = $self->simplereq(
+                'open-ils.trigger',
+                $auto_method, $def->id, $object, $context_org, $user_data
+            );
+
+            $resp = $self->simplereq(
+                'open-ils.trigger',
+                'open-ils.trigger.event.fire',
+                $event_id
+            );
+        } else {
+            $event_id = $self->patientreq(
+                $client,
+                'open-ils.trigger',
+                $auto_method, $def->id, $object, $context_org, $user_data
+            );
+
+            $resp = $self->patientreq(
+                $client,
+                'open-ils.trigger',
+                'open-ils.trigger.event.fire',
+                $event_id
+            );
+        }
+        
+        if($resp and $resp->{event}) {
+            $e->xact_begin;
+            $final_resp = $e->retrieve_action_trigger_event([
+                $resp->{event}->id,
+                {flesh => 1, flesh_fields => {atev => ['template_output', 'error_output']}}
+            ]);
+            $e->rollback;
+        }
+    }
+
+    return $final_resp;
+}
+
+
+sub create_events_for_hook {
+    my($self, $hook, $obj, $org_id, $granularity, $user_data, $wait) = @_;
+    my $ses = OpenSRF::AppSession->create('open-ils.trigger');
+    my $req = $ses->request('open-ils.trigger.event.autocreate', 
+        $hook, $obj, $org_id, $granularity, $user_data);
+    return undef unless $wait;
+    my $resp = $req->recv;
+    return $resp->content if $resp;
+}
+
+sub create_uuid_string {
+    return create_UUID_as_string();
+}
+
+sub create_circ_chain_summary {
+    my($class, $e, $circ_id) = @_;
+    my $sum = $e->json_query({from => ['action.summarize_circ_chain', $circ_id]})->[0];
+    return undef unless $sum;
+    my $obj = Fieldmapper::action::circ_chain_summary->new;
+    $obj->$_($sum->{$_}) for keys %$sum;
+    return $obj;
+}
+
+
+# Returns "mra" attribute key/value pairs for a set of bre's
+# Takes a list of bre IDs, returns a hash of hashes,
+# {bre_id1 => {key1 => {code => value1, label => label1}, ...}...}
+my $ccvm_cache;
+sub get_bre_attrs {
+    my ($class, $bre_ids, $e) = @_;
+    $e = $e || OpenILS::Utils::CStoreEditor->new;
+
+    my $attrs = {};
+    return $attrs unless defined $bre_ids;
+    $bre_ids = [$bre_ids] unless ref $bre_ids;
+
+    my $mra = $e->json_query({
+        select => {
+            mra => [
+                {
+                    column => 'id',
+                    alias => 'bre'
+                }, {
+                    column => 'attrs',
+                    transform => 'each',
+                    result_field => 'key',
+                    alias => 'key'
+                },{
+                    column => 'attrs',
+                    transform => 'each',
+                    result_field => 'value',
+                    alias => 'value'
+                }
+            ]
+        },
+        from => 'mra',
+        where => {id => $bre_ids}
+    });
+
+    return $attrs unless $mra;
+
+    $ccvm_cache = $ccvm_cache || $e->search_config_coded_value_map({id => {'!=' => undef}});
+
+    for my $id (@$bre_ids) {
+        $attrs->{$id} = {};
+        for my $mra (grep { $_->{bre} eq $id } @$mra) {
+            my $ctype = $mra->{key};
+            my $code = $mra->{value};
+            $attrs->{$id}->{$ctype} = {code => $code};
+            if($code) {
+                my ($ccvm) = grep { $_->ctype eq $ctype and $_->code eq $code } @$ccvm_cache;
+                $attrs->{$id}->{$ctype}->{label} = $ccvm->value if $ccvm;
+            }
+        }
+    }
+
+    return $attrs;
+}
+
+# Shorter version of bib_container_items_via_search() below, only using
+# the queryparser record_list filter instead of the container filter.
+sub bib_record_list_via_search {
+    my ($class, $search_query, $search_args) = @_;
+
+    # First, Use search API to get container items sorted in any way that crad
+    # sorters support.
+    my $search_result = $class->simplereq(
+        "open-ils.search", "open-ils.search.biblio.multiclass.query",
+        $search_args, $search_query
+    );
+
+    unless ($search_result) {
+        # empty result sets won't cause this, but actual errors should.
+        $logger->warn("bib_record_list_via_search() got nothing from search");
+        return;
+    }
+
+    # Throw away other junk from search, keeping only bib IDs.
+    return [ map { pop @$_ } @{$search_result->{ids}} ];
+}
+
+# 'no_flesh' avoids fleshing the target_biblio_record_entry
+sub bib_container_items_via_search {
+    my ($class, $container_id, $search_query, $search_args, $no_flesh) = @_;
+
+    # First, Use search API to get container items sorted in any way that crad
+    # sorters support.
+    my $search_result = $class->simplereq(
+        "open-ils.search", "open-ils.search.biblio.multiclass.query",
+        $search_args, $search_query
+    );
+    unless ($search_result) {
+        # empty result sets won't cause this, but actual errors should.
+        $logger->warn("bib_container_items_via_search() got nothing from search");
+        return;
+    }
+
+    # Throw away other junk from search, keeping only bib IDs.
+    my $id_list = [ map { pop @$_ } @{$search_result->{ids}} ];
+
+    return [] unless @$id_list;
+
+    # Now get the bib container items themselves...
+    my $e = new OpenILS::Utils::CStoreEditor;
+    unless ($e) {
+        $logger->warn("bib_container_items_via_search() couldn't get cstoreeditor");
+        return;
+    }
+
+    my @flesh_fields = qw/notes/;
+    push(@flesh_fields, 'target_biblio_record_entry') unless $no_flesh;
+
+    my $items = $e->search_container_biblio_record_entry_bucket_item([
+        {
+            "target_biblio_record_entry" => $id_list,
+            "bucket" => $container_id
+        }, {
+            flesh => 1,
+            flesh_fields => {"cbrebi" => \@flesh_fields}
+        }
+    ]);
+    unless ($items) {
+        $logger->warn(
+            "bib_container_items_via_search() couldn't get bucket items: " .
+            $e->die_event->{textcode}
+        );
+        return;
+    }
+
+    # ... and put them in the same order that the search API said they
+    # should be in.
+    my %ordering_hash = map { 
+        ($no_flesh) ? $_->target_biblio_record_entry : $_->target_biblio_record_entry->id, 
+        $_ 
+    } @$items;
+
+    return [map { $ordering_hash{$_} } @$id_list];
+}
+
+# returns undef on success, Event on error
+sub log_user_activity {
+    my ($class, $user_id, $who, $what, $e, $async) = @_;
+
+    my $commit = 0;
+    if (!$e) {
+        $e = OpenILS::Utils::CStoreEditor->new(xact => 1);
+        $commit = 1;
+    }
+
+    my $res = $e->json_query({
+        from => [
+            'actor.insert_usr_activity', 
+            $user_id, $who, $what, OpenSRF::AppSession->ingress
+        ]
+    });
+
+    if ($res) { # call returned OK
+
+        $e->commit   if $commit and @$res;
+        $e->rollback if $commit and !@$res;
+
+    } else {
+        return $e->die_event;
+    }
+
+    return undef;
+}
+
+# I hate to put this here exactly, but this code needs to be shared between
+# the TPAC's mod_perl module and open-ils.serial.
+#
+# There is a reason every part of the query *except* those parts dealing
+# with scope are moved here from the code's origin in TPAC.  The serials
+# use case does *not* want the same scoping logic.
+#
+# Also, note that for the serials uses case, we may filter in OPAC visible
+# status and copy/call_number deletedness, but we don't filter on any
+# particular values for serial.item.status or serial.item.date_received.
+# Since we're only using this *after* winnowing down the set of issuances
+# that copies should be related to, I'm not sure we need any such serial.item
+# filters.
+
+sub basic_opac_copy_query {
+    ######################################################################
+    # Pass a defined value for either $rec_id OR ($iss_id AND $dist_id), #
+    # not both.                                                          #
+    ######################################################################
+    my ($self,$rec_id,$iss_id,$dist_id,$copy_limit,$copy_offset,$staff) = @_;
+
+    return {
+        select => {
+            acp => ['id', 'barcode', 'circ_lib', 'create_date',
+                    'age_protect', 'holdable'],
+            acpl => [
+                {column => 'name', alias => 'copy_location'},
+                {column => 'holdable', alias => 'location_holdable'}
+            ],
+            ccs => [
+                {column => 'name', alias => 'copy_status'},
+                {column => 'holdable', alias => 'status_holdable'}
+            ],
+            acn => [
+                {column => 'label', alias => 'call_number_label'},
+                {column => 'id', alias => 'call_number'}
+            ],
+            circ => ['due_date'],
+            acnp => [
+                {column => 'label', alias => 'call_number_prefix_label'},
+                {column => 'id', alias => 'call_number_prefix'}
+            ],
+            acns => [
+                {column => 'label', alias => 'call_number_suffix_label'},
+                {column => 'id', alias => 'call_number_suffix'}
+            ],
+            bmp => [
+                {column => 'label', alias => 'part_label'},
+            ],
+            ($iss_id ? (sitem => ["issuance"]) : ())
+        },
+
+        from => {
+            acp => {
+                ($iss_id ? (
+                    sitem => {
+                        fkey => 'id',
+                        field => 'unit',
+                        filter => {issuance => $iss_id},
+                        join => {
+                            sstr => { }
+                        }
+                    }
+                ) : ()),
+                acn => {
+                    join => {
+                        acnp => { fkey => 'prefix' },
+                        acns => { fkey => 'suffix' }
+                    },
+                    filter => [
+                        {deleted => 'f'},
+                        ($rec_id ? {record => $rec_id} : ())
+                    ],
+                },
+                circ => { # If the copy is circulating, retrieve the open circ
+                    type => 'left',
+                    filter => {checkin_time => undef}
+                },
+                acpl => {
+                    ($staff ? () : (filter => { opac_visible => 't' }))
+                },
+                ccs => {
+                    ($staff ? () : (filter => { opac_visible => 't' }))
+                },
+                aou => {},
+                acpm => {
+                    type => 'left',
+                    join => {
+                        bmp => { type => 'left' }
+                    }
+                }
+            }
+        },
+
+        where => {
+            '+acp' => {
+                deleted => 'f',
+                ($staff ? () : (opac_visible => 't'))
+            },
+            ($dist_id ? ( '+sstr' => { distribution => $dist_id } ) : ()),
+            ($staff ? () : ( '+aou' => { opac_visible => 't' } ))
+        },
+
+        order_by => [
+            {class => 'aou', field => 'name'},
+            {class => 'acn', field => 'label'}
+        ],
+
+        limit => $copy_limit,
+        offset => $copy_offset
+    };
+}
+
+# Compare two dates, date1 and date2. If date2 is not defined, then
+# DateTime->now will be used. Assumes dates are in ISO8601 format as
+# supported by DateTime::Format::ISO8601. (A future enhancement might
+# be to support other formats.)
+#
+# Returns -1 if $date1 < $date2
+# Returns 0 if $date1 == $date2
+# Returns 1 if $date1 > $date2
+sub datecmp {
+    my $self = shift;
+    my $date1 = shift;
+    my $date2 = shift;
+
+    # Check for timezone offsets and limit them to 2 digits:
+    if ($date1 && $date1 =~ /(?:-|\+)\d\d\d\d$/) {
+        $date1 = substr($date1, 0, length($date1) - 2);
+    }
+    if ($date2 && $date2 =~ /(?:-|\+)\d\d\d\d$/) {
+        $date2 = substr($date2, 0, length($date2) - 2);
+    }
+
+    # check date1:
+    unless (UNIVERSAL::isa($date1, "DateTime")) {
+        $date1 = DateTime::Format::ISO8601->parse_datetime($date1);
+    }
+
+    # Check for date2:
+    unless ($date2) {
+        $date2 = DateTime->now;
+    } else {
+        unless (UNIVERSAL::isa($date2, "DateTime")) {
+            $date2 = DateTime::Format::ISO8601->parse_datetime($date2);
+        }
+    }
+
+    return DateTime->compare($date1, $date2);
+}
+
+
+# marcdoc is an XML::LibXML document
+# updates the doc and returns the entityized MARC string
+sub strip_marc_fields {
+    my ($class, $e, $marcdoc, $grps) = @_;
+    
+    my $orgs = $class->get_org_ancestors($e->requestor->ws_ou);
+
+    my $query = {
+        select  => {vibtf => ['field']},
+        from    => {vibtf => 'vibtg'},
+        where   => {'+vibtg' => {owner => $orgs}},
+        distinct => 1
+    };
+
+    # give me always-apply groups plus any selected groups
+    if ($grps and @$grps) {
+        $query->{where}->{'+vibtg'}->{'-or'} = [
+            {id => $grps},
+            {always_apply => 't'}
+        ];
+
+    } else {
+        $query->{where}->{'+vibtg'}->{always_apply} = 't';
+    }
+
+    my $fields = $e->json_query($query);
+
+    for my $field (@$fields) {
+        my $tag = $field->{field};
+        for my $node ($marcdoc->findnodes('//*[@tag="'.$tag.'"]')) {
+            $node->parentNode->removeChild($node);
+        }
+    }
+
+       return $class->entityize($marcdoc->documentElement->toString);
+}
+
+
+1;
+
diff --git a/src/perl/lib/ShareStuff/CStoreEditor.pm b/src/perl/lib/ShareStuff/CStoreEditor.pm
new file mode 100644 (file)
index 0000000..392cc74
--- /dev/null
@@ -0,0 +1,929 @@
+use strict; use warnings;
+package ShareStuff::CStoreEditor;
+use ShareStuff::AppUtils;
+use OpenSRF::Application;
+use OpenSRF::AppSession;
+use OpenSRF::EX qw(:try);
+use Fieldmapper;
+use ShareStuff::Event;
+use Data::Dumper;
+use OpenSRF::Utils::JSON;
+use OpenSRF::Utils::Logger qw($logger);
+my $U = "ShareStuff::AppUtils";
+my %PERMS;
+my $cache;
+my %xact_ed_cache;
+
+# if set, we will use this locale for all new sessions
+# if unset, we rely on the existing opensrf locale propagation
+our $default_locale;
+
+our $always_xact = 0;
+our $_loaded = 1;
+
+#my %PERMS = (
+#      'biblio.record_entry'   => { update => 'UPDATE_MARC' },
+#      'asset.copy'                            => { update => 'UPDATE_COPY'},
+#      'asset.call_number'             => { update => 'UPDATE_VOLUME'},
+#      'action.circulation'            => { retrieve => 'VIEW_CIRCULATIONS'},
+#);
+
+sub flush_forced_xacts {
+    for my $k ( keys %xact_ed_cache ) {
+        try {
+            $xact_ed_cache{$k}->rollback;
+        } catch Error with {
+            # rollback failed
+        };
+        delete $xact_ed_cache{$k};
+    }
+}
+
+# -----------------------------------------------------------------------------
+# Export some useful functions
+# -----------------------------------------------------------------------------
+use vars qw(@EXPORT_OK %EXPORT_TAGS);
+use Exporter;
+use base qw/Exporter/;
+push @EXPORT_OK, ( 'new_editor', 'new_rstore_editor' );
+%EXPORT_TAGS = ( funcs => [ qw/ new_editor new_rstore_editor / ] );
+
+sub new_editor { return ShareStuff::CStoreEditor->new(@_); }
+
+sub new_rstore_editor { 
+       my $e = ShareStuff::CStoreEditor->new(@_); 
+       $e->app('open-ils.reporter-store');
+       return $e;
+}
+
+
+# -----------------------------------------------------------------------------
+# Log levels
+# -----------------------------------------------------------------------------
+use constant E => 'error';
+use constant W => 'warn';
+use constant I => 'info';
+use constant D => 'debug';
+use constant A => 'activity';
+
+
+
+# -----------------------------------------------------------------------------
+# Params include:
+#      xact=><true> : creates a storage transaction
+#      authtoken=>$token : the login session key
+# -----------------------------------------------------------------------------
+sub new {
+       my( $class, %params ) = @_;
+       $class = ref($class) || $class;
+       my $self = bless( \%params, $class );
+       $self->{checked_perms} = {};
+       return $self;
+}
+
+sub DESTROY {
+        my $self = shift;
+        $self->reset;
+        return undef;
+}
+
+sub app {
+       my( $self, $app ) = @_;
+       $self->{app} = $app if $app;
+       $self->{app} = 'open-ils.cstore' unless $self->{app};
+       return $self->{app};
+}
+
+
+# -----------------------------------------------------------------------------
+# Log the editor metadata along with the log string
+# -----------------------------------------------------------------------------
+sub log {
+       my( $self, $lev, $str ) = @_;
+       my $s = "editor[";
+    if ($always_xact) {
+        $s .= "!|";
+    } elsif ($self->{xact}) {
+        $s .= "1|";
+    } else {
+           $s .= "0|";
+    }
+       $s .= "0" unless $self->requestor;
+       $s .= $self->requestor->id if $self->requestor;
+       $s .= "]";
+       $logger->$lev("$s $str");
+}
+
+# -----------------------------------------------------------------------------
+# Verifies the auth token and fetches the requestor object
+# -----------------------------------------------------------------------------
+sub checkauth {
+       my $self = shift;
+       $self->log(D, "checking auth token ".$self->authtoken);
+
+       my $content = $U->simplereq( 
+               'open-ils.auth', 
+               'open-ils.auth.session.retrieve', $self->authtoken, 1);
+
+    if(!$content or $U->event_code($content)) {
+        $self->event( ($content) ? $content : ShareStuff::Event->new('NO_SESSION'));
+        return undef;
+    }
+
+    $self->{authtime} = $content->{authtime};
+       return $self->{requestor} = $content->{userobj};
+}
+
+=head1 test
+
+sub checkauth {
+       my $self = shift;
+       $cache = OpenSRF::Utils::Cache->new('global') unless $cache;
+       $self->log(D, "checking cached auth token ".$self->authtoken);
+       my $user = $cache->get_cache("oils_auth_".$self->authtoken);
+       return $self->{requestor} = $user->{userobj} if $user;
+       $self->event(ShareStuff::Event->new('NO_SESSION'));
+       return undef;
+}
+
+=cut
+
+
+# -----------------------------------------------------------------------------
+# Returns the last generated event
+# -----------------------------------------------------------------------------
+sub event {
+       my( $self, $evt ) = @_;
+       $self->{event} = $evt if $evt;
+       return $self->{event};
+}
+
+# -----------------------------------------------------------------------------
+# Destroys the transaction and disconnects where necessary,
+# then returns the last event that occurred
+# -----------------------------------------------------------------------------
+sub die_event {
+       my $self = shift;
+    my $evt = shift;
+       $self->rollback;
+    $self->died(1);
+    $self->event($evt);
+       return $self->event;
+}
+
+
+# -----------------------------------------------------------------------------
+# Clears the last caught event
+# -----------------------------------------------------------------------------
+sub clear_event {
+       my $self = shift;
+       $self->{event} = undef;
+}
+
+sub died {
+    my($self, $died) = @_;
+    $self->{died} = $died if defined $died;
+    return $self->{died};
+}
+
+sub authtoken {
+       my( $self, $auth ) = @_;
+       $self->{authtoken} = $auth if $auth;
+       return $self->{authtoken};
+}
+
+sub authtime {
+       my( $self, $auth ) = @_;
+       $self->{authtime} = $auth if $auth;
+       return $self->{authtime};
+}
+
+sub timeout {
+    my($self, $to) = @_;
+    $self->{timeout} = $to if defined $to;
+    return defined($self->{timeout}) ? $self->{timeout} : 60;
+}
+
+# -----------------------------------------------------------------------------
+# fetches the session, creating if necessary.  If 'xact' is true on this
+# object, a db session is created
+# -----------------------------------------------------------------------------
+sub session {
+       my( $self, $session ) = @_;
+       $self->{session} = $session if $session;
+
+       # sessions can stick around longer than a single request/transaction.
+       # kill it if our default locale was altered since the last request
+       # and it does not match the locale of the existing session.
+       delete $self->{session} if
+               $default_locale and
+               $self->{session} and
+               $self->{session}->session_locale ne $default_locale;
+
+       if(!$self->{session}) {
+               $self->{session} = OpenSRF::AppSession->create($self->app);
+               $self->{session}->session_locale($default_locale) if $default_locale;
+
+               if( ! $self->{session} ) {
+                       my $str = "Error creating cstore session with OpenSRF::AppSession->create()!";
+                       $self->log(E, $str);
+                       throw OpenSRF::EX::ERROR ($str);
+               }
+
+               $self->{session}->connect if $self->{xact} or $self->{connect} or $always_xact;
+               $self->xact_begin if $self->{xact} or $always_xact;
+       }
+
+    $xact_ed_cache{$self->{xact_id}} = $self if $always_xact and $self->{xact_id};
+       return $self->{session};
+}
+
+
+# -----------------------------------------------------------------------------
+# Starts a storage transaction
+# -----------------------------------------------------------------------------
+sub xact_begin {
+    my $self = shift;
+    return $self->{xact_id} if $self->{xact_id};
+    $self->session->connect unless $self->session->state == OpenSRF::AppSession::CONNECTED();
+       $self->log(D, "starting new database transaction");
+       unless($self->{xact_id}) {
+           my $stat = $self->request($self->app . '.transaction.begin');
+           $self->log(E, "error starting database transaction") unless $stat;
+        $self->{xact_id} = $stat;
+        if($self->authtoken) {
+            if(!$self->requestor) {
+                $self->checkauth;
+            }
+            my $user_id = undef;
+            my $ws_id = undef;
+            if($self->requestor) {
+                $user_id = $self->requestor->id;
+                $ws_id = $self->requestor->wsid;
+            }
+            $self->request($self->app . '.set_audit_info', $self->authtoken, $user_id, $ws_id);
+        }
+    }
+    $self->{xact} = 1;
+    return $self->{xact_id};
+}
+
+# -----------------------------------------------------------------------------
+# Commits a storage transaction
+# -----------------------------------------------------------------------------
+sub xact_commit {
+       my $self = shift;
+    return unless $self->{xact_id};
+       $self->log(D, "comitting db session");
+       my $stat = $self->request($self->app.'.transaction.commit');
+       $self->log(E, "error comitting database transaction") unless $stat;
+    delete $self->{xact_id};
+    delete $self->{xact};
+       return $stat;
+}
+
+# -----------------------------------------------------------------------------
+# Rolls back a storage stransaction
+# -----------------------------------------------------------------------------
+sub xact_rollback {
+       my $self = shift;
+    return unless $self->{session} and $self->{xact_id};
+       $self->log(I, "rolling back db session");
+       my $stat = $self->request($self->app.".transaction.rollback");
+       $self->log(E, "error rolling back database transaction") unless $stat;
+    delete $self->{xact_id};
+    delete $self->{xact};
+       return $stat;
+}
+
+
+# -----------------------------------------------------------------------------
+# Savepoint functions.  If no savepoint name is provided, the same name is used 
+# for each successive savepoint, in which case only the last savepoint set can 
+# be released or rolled back.
+# -----------------------------------------------------------------------------
+sub set_savepoint {
+    my $self = shift;
+    my $name = shift || 'savepoint';
+    return unless $self->{session} and $self->{xact_id};
+       $self->log(I, "setting savepoint '$name'");
+       my $stat = $self->request($self->app.".savepoint.set", $name)
+           or $self->log(E, "error setting savepoint '$name'");
+    return $stat;
+}
+
+sub release_savepoint {
+    my $self = shift;
+    my $name = shift || 'savepoint';
+    return unless $self->{session} and $self->{xact_id};
+       $self->log(I, "releasing savepoint '$name'");
+       my $stat = $self->request($self->app.".savepoint.release", $name)
+        or $self->log(E, "error releasing savepoint '$name'");
+    return $stat;
+}
+
+sub rollback_savepoint {
+    my $self = shift;
+    my $name = shift || 'savepoint';
+    return unless $self->{session} and $self->{xact_id};
+       $self->log(I, "rollback savepoint '$name'");
+       my $stat = $self->request($self->app.".savepoint.rollback", $name)
+        or $self->log(E, "error rolling back savepoint '$name'");
+    return $stat;
+}
+
+
+# -----------------------------------------------------------------------------
+# Rolls back the transaction and disconnects
+# -----------------------------------------------------------------------------
+sub rollback {
+       my $self = shift;
+    my $err;
+    my $ret;
+       try {
+        $self->xact_rollback;
+    } catch Error with  {
+        $err = shift
+    } finally {
+        $ret = $self->disconnect
+    };
+    throw $err if ($err);
+    return $ret;
+}
+
+sub disconnect {
+       my $self = shift;
+       $self->session->disconnect if 
+        $self->{session} and 
+        $self->{session}->state == OpenSRF::AppSession::CONNECTED();
+    delete $self->{session};
+}
+
+
+# -----------------------------------------------------------------------------
+# commits the db session and destroys the session
+# returns the status of the commit call
+# -----------------------------------------------------------------------------
+sub commit {
+       my $self = shift;
+       return unless $self->{xact_id};
+       my $stat = $self->xact_commit;
+    $self->disconnect;
+    return $stat;
+}
+
+# -----------------------------------------------------------------------------
+# clears all object data. Does not commit the db transaction.
+# -----------------------------------------------------------------------------
+sub reset {
+       my $self = shift;
+       $self->disconnect;
+       $$self{$_} = undef for (keys %$self);
+}
+
+
+# -----------------------------------------------------------------------------
+# commits and resets
+# -----------------------------------------------------------------------------
+sub finish {
+       my $self = shift;
+    my $err;
+    my $ret;
+       try {
+        $self->commit;
+    } catch Error with  {
+        $err = shift
+    } finally {
+        $ret = $self->reset
+    };
+    throw $err if ($err);
+    return $ret;
+}
+
+
+
+# -----------------------------------------------------------------------------
+# Does a simple storage request
+# -----------------------------------------------------------------------------
+sub request {
+       my( $self, $method, @params ) = @_;
+
+    my $val;
+       my $err;
+       my $argstr = __arg_to_string( (scalar(@params)) == 1 ? $params[0] : \@params);
+       my $locale = $self->session->session_locale;
+
+       $self->log(I, "request $locale $method $argstr");
+
+       if( ($self->{xact} or $always_xact) and 
+                       $self->session->state != OpenSRF::AppSession::CONNECTED() ) {
+               #$logger->error("CStoreEditor lost it's connection!!");
+               throw OpenSRF::EX::ERROR ("CStore connection timed out - transaction cannot continue");
+       }
+
+
+       try {
+
+        my $req = $self->session->request($method, @params);
+
+        if($self->substream) {
+            $self->log(D,"running in substream mode");
+            $val = [];
+            while( my $resp = $req->recv(timeout => $self->timeout) ) {
+                push(@$val, $resp->content) if $resp->content and not $self->discard;
+            }
+
+        } else {
+            my $resp = $req->recv(timeout => $self->timeout);
+            if($req->failed) {
+                $err = $resp;
+                       $self->log(E, "request error $method : $argstr : $err");
+            } else {
+                $val = $resp->content if $resp;
+            }
+        }
+
+        $req->finish;
+
+       } catch Error with {
+               $err = shift;
+               $self->log(E, "request error $method : $argstr : $err");
+       };
+
+       throw $err if $err;
+       return $val;
+}
+
+sub substream {
+   my( $self, $bool ) = @_;
+   $self->{substream} = $bool if defined $bool;
+   return $self->{substream};
+}
+
+# -----------------------------------------------------------------------------
+# discard response data instead of returning it to the caller.  currently only 
+# works in conjunction with substream mode.  
+# -----------------------------------------------------------------------------
+sub discard {
+   my( $self, $bool ) = @_;
+   $self->{discard} = $bool if defined $bool;
+   return $self->{discard};
+}
+
+
+# -----------------------------------------------------------------------------
+# Sets / Returns the requestor object.  This is set when checkauth succeeds.
+# -----------------------------------------------------------------------------
+sub requestor {
+       my($self, $requestor) = @_;
+       $self->{requestor} = $requestor if $requestor;
+       return $self->{requestor};
+}
+
+
+
+# -----------------------------------------------------------------------------
+# Holds the last data received from a storage call
+# -----------------------------------------------------------------------------
+sub data {
+       my( $self, $data ) = @_;
+       $self->{data} = $data if defined $data;
+       return $self->{data};
+}
+
+
+# -----------------------------------------------------------------------------
+# True if this perm has already been checked at this org
+# -----------------------------------------------------------------------------
+sub perm_checked {
+       my( $self, $perm, $org ) = @_;
+       $self->{checked_perms}->{$org} = {}
+               unless $self->{checked_perms}->{$org};
+       my $checked = $self->{checked_perms}->{$org}->{$perm};
+       if(!$checked) {
+               $self->{checked_perms}->{$org}->{$perm} = 1;
+               return 0;
+       }
+       return 1;
+}
+
+
+
+# -----------------------------------------------------------------------------
+# Returns true if the requested perm is allowed.  If the perm check fails,
+# $e->event is set and undef is returned
+# The perm user is $e->requestor->id and perm org defaults to the requestor's
+# ws_ou
+# if perm is an array of perms, method will return true at the first allowed
+# permission.  If none of the perms are allowed, the perm_failure event
+# is created with the last perm to fail
+# -----------------------------------------------------------------------------
+my $PERM_QUERY = {
+    select => {
+        au => [ {
+            transform => 'permission.usr_has_perm',
+            alias => 'has_perm',
+            column => 'id',
+            params => []
+        } ]
+    },
+    from => 'au',
+    where => {},
+};
+
+my $OBJECT_PERM_QUERY = {
+    select => {
+        au => [ {
+            transform => 'permission.usr_has_object_perm',
+            alias => 'has_perm',
+            column => 'id',
+            params => []
+        } ]
+    },
+    from => 'au',
+    where => {},
+};
+
+sub allowed {
+       my( $self, $perm, $org, $object, $hint ) = @_;
+       my $uid = $self->requestor->id;
+       $org ||= $self->requestor->ws_ou;
+
+    my $perms = (ref($perm) eq 'ARRAY') ? $perm : [$perm];
+
+    for $perm (@$perms) {
+           $self->log(I, "checking perms user=$uid, org=$org, perm=$perm");
+    
+        if($object) {
+            my $params;
+            if(ref $object) {
+                # determine the ID field and json_hint from the object
+                my $id_field = $object->Identity;
+                $params = [$perm, $object->json_hint, $object->$id_field];
+            } else {
+                # we were passed an object-id and json_hint
+                $params = [$perm, $hint, $object];
+            }
+            push(@$params, $org) if $org;
+            $OBJECT_PERM_QUERY->{select}->{au}->[0]->{params} = $params;
+            $OBJECT_PERM_QUERY->{where}->{id} = $uid;
+            return 1 if $U->is_true($self->json_query($OBJECT_PERM_QUERY)->[0]->{has_perm});
+
+        } else {
+            $PERM_QUERY->{select}->{au}->[0]->{params} = [$perm, $org];
+            $PERM_QUERY->{where}->{id} = $uid;
+            return 1 if $U->is_true($self->json_query($PERM_QUERY)->[0]->{has_perm});
+        }
+    }
+
+    # set the perm failure event if the permission check returned false
+       my $e = ShareStuff::Event->new('PERM_FAILURE', ilsperm => $perm, ilspermloc => $org);
+       $self->event($e);
+       return undef;
+}
+
+
+# -----------------------------------------------------------------------------
+# Returns the list of object IDs this user has object-specific permissions for
+# -----------------------------------------------------------------------------
+sub objects_allowed {
+    my($self, $perm, $obj_type) = @_;
+
+    my $perms = (ref($perm) eq 'ARRAY') ? $perm : [$perm];
+    my @ids;
+
+    for $perm (@$perms) {
+        my $query = {
+            select => {puopm => ['object_id']},
+            from => {
+                puopm => {
+                    ppl => {field => 'id',fkey => 'perm'}
+                }
+            },
+            where => {
+                '+puopm' => {usr => $self->requestor->id, object_type => $obj_type},
+                '+ppl' => {code => $perm}
+            }
+        };
+    
+        my $list = $self->json_query($query);
+        push(@ids, 0+$_->{object_id}) for @$list;
+    }
+
+   my %trim;
+   $trim{$_} = 1 for @ids;
+   return [ keys %trim ];
+}
+
+
+# -----------------------------------------------------------------------------
+# checks the appropriate perm for the operation
+# -----------------------------------------------------------------------------
+sub _checkperm {
+       my( $self, $ptype, $action, $org ) = @_;
+       $org ||= $self->requestor->ws_ou;
+       my $perm = $PERMS{$ptype}{$action};
+       if( $perm ) {
+               return undef if $self->perm_checked($perm, $org);
+               return $self->event unless $self->allowed($perm, $org);
+       } else {
+               $self->log(I, "no perm provided for $ptype.$action");
+       }
+       return undef;
+}
+
+
+
+# -----------------------------------------------------------------------------
+# Logs update actions to the activity log
+# -----------------------------------------------------------------------------
+sub log_activity {
+       my( $self, $method, $type, $action, $arg ) = @_;
+       my $str = "$type.$action";
+
+    if ($arg) {
+        
+        my $redact;
+
+        if ($OpenSRF::Application::shared_conf and
+            $OpenSRF::Application::shared_conf->shared and
+            $redact = $OpenSRF::Application::shared_conf->shared->log_protect and
+            ref($redact) eq 'ARRAY' and
+            grep { $method =~ /^$_/ } @{$redact}) {
+
+                # when API calls are marked as log-protect, avoid
+                # dumping the param object to the activity log.
+                $str .= " **DETAILS REDACTED**";
+        } else {
+
+            $str .= _prop_string($arg);
+        }
+    }
+
+
+       $self->log(A, $str);
+}
+
+
+
+sub _prop_string {
+       my $obj = shift;
+       my @props = $obj->properties;
+       my $str = "";
+       for(@props) {
+               my $prop = $obj->$_() || "";
+               $prop = substr($prop, 0, 128) . "..." if length $prop > 131;
+               $str .= " $_=$prop";
+       }
+       return $str;
+}
+
+
+sub __arg_to_string {
+       my $arg = shift;
+       return "" unless defined $arg;
+       if( UNIVERSAL::isa($arg, "Fieldmapper") ) {
+        my $idf = $arg->Identity;
+               return (defined $arg->$idf) ? $arg->$idf : '<new object>';
+       }
+       return OpenSRF::Utils::JSON->perl2JSON($arg);
+       return "";
+}
+
+
+# -----------------------------------------------------------------------------
+# This does the actual storage query.
+#
+# 'search' calls become search_where calls and $arg can be a search hash or
+# an array-ref of storage search options.  
+#
+# 'retrieve' expects an id
+# 'update' expects an object
+# 'create' expects an object
+# 'delete' expects an object
+#
+# All methods return true on success and undef on failure.  On failure, 
+# $e->event is set to the generated event.  
+# Note: this method assumes that updating a non-changed object and 
+# thereby receiving a 0 from storage, is a successful update.  
+#
+# The method will therefore return true so the caller can just do 
+# $e->update_blah($x) or return $e->event;
+# The true value returned from storage for all methods will be stored in 
+# $e->data, until the next method is called.
+#
+# not-found events are generated on retrieve and serach methods.
+# action=search methods will return [] (==true) if no data is found.  If the
+# caller is interested in the not found event, they can do:  
+# return $e->event unless @$results; 
+# -----------------------------------------------------------------------------
+sub runmethod {
+       my( $self, $action, $type, $arg, $options ) = @_;
+
+   $options ||= {};
+
+       if( $action eq 'retrieve' ) {
+               if(! defined($arg) ) {
+                       $self->log(W,"$action $type called with no ID...");
+                       $self->event(_mk_not_found($type, $arg));
+                       return undef;
+               } elsif( ref($arg) =~ /Fieldmapper/ ) {
+                       $self->log(D,"$action $type called with an object.. attempting Identity retrieval..");
+            my $idf = $arg->Identity;
+                       $arg = $arg->$idf;
+               }
+       }
+
+       my @arg = ( ref($arg) eq 'ARRAY' ) ? @$arg : ($arg);
+       my $method = $self->app.".direct.$type.$action";
+
+       if( $action eq 'search' ) {
+               $method .= '.atomic';
+
+       } elsif( $action eq 'batch_retrieve' ) {
+               $action = 'search';
+               $method =~ s/batch_retrieve/search/o;
+               $method .= '.atomic';
+               my $tt = $type;
+               $tt =~ s/\./::/og;
+               my $fmobj = "Fieldmapper::$tt";
+               my $ident_field = $fmobj->Identity;
+
+               if (ref $arg[0] eq 'ARRAY') {
+                       # $arg looks like: ([1, 2, 3], {search_args})
+                       @arg = ( { $ident_field => $arg[0] }, @arg[1 .. $#arg] );
+               } else {
+                       # $arg looks like: [1, 2, 3]
+                       @arg = ( { $ident_field => $arg } );
+               }
+
+       } elsif( $action eq 'retrieve_all' ) {
+               $action = 'search';
+               $method =~ s/retrieve_all/search/o;
+               my $tt = $type;
+               $tt =~ s/\./::/og;
+               my $fmobj = "Fieldmapper::$tt";
+               @arg = ( { $fmobj->Identity => { '!=' => undef } } );
+               $method .= '.atomic';
+       }
+
+       $method =~ s/search/id_list/o if $options->{idlist};
+
+    $method =~ s/\.atomic$//o if $self->substream($$options{substream} || 0);
+    $self->timeout($$options{timeout});
+    $self->discard($$options{discard});
+
+       # remove any stale events
+       $self->clear_event;
+
+       if( $action eq 'update' or $action eq 'delete' or $action eq 'create' ) {
+               if(!($self->{xact} or $always_xact)) {
+                       $logger->error("Attempt to update DB while not in a transaction : $method");
+                       throw OpenSRF::EX::ERROR ("Attempt to update DB while not in a transaction : $method");
+               }
+               $self->log_activity($method, $type, $action, $arg);
+       }
+
+       if($$options{checkperm}) {
+               my $a = ($action eq 'search') ? 'retrieve' : $action;
+               my $e = $self->_checkperm($type, $a, $$options{permorg});
+               if($e) {
+                       $self->event($e);
+                       return undef;
+               }
+       }
+
+       my $obj; 
+       my $err = '';
+
+       try {
+               $obj = $self->request($method, @arg);
+       } catch Error with { $err = shift; };
+       
+
+       if(!defined $obj) {
+               $self->log(I, "request returned no data : $method");
+
+               if( $action eq 'retrieve' ) {
+                       $self->event(_mk_not_found($type, $arg));
+
+               } elsif( $action eq 'update' or 
+                               $action eq 'delete' or $action eq 'create' ) {
+                       my $evt = ShareStuff::Event->new(
+                               'DATABASE_UPDATE_FAILED', payload => $arg, debug => "$err" );
+                       $self->event($evt);
+               }
+
+               if( $err ) {
+                       $self->event( 
+                               ShareStuff::Event->new( 'DATABASE_QUERY_FAILED', 
+                                       payload => $arg, debug => "$err" ));
+                       return undef;
+               }
+
+               return undef;
+       }
+
+       if( $action eq 'create' and $obj == 0 ) {
+               my $evt = ShareStuff::Event->new(
+                       'DATABASE_UPDATE_FAILED', payload => $arg, debug => "$err" );
+               $self->event($evt);
+               return undef;
+       }
+
+       # If we havn't dealt with the error in a nice way, go ahead and throw it
+       if( $err ) {
+               $self->event( 
+                       ShareStuff::Event->new( 'DATABASE_QUERY_FAILED', 
+                               payload => $arg, debug => "$err" ));
+               return undef;
+       }
+
+       if( $action eq 'search' ) {
+               $self->log(I, "$type.$action : returned ".scalar(@$obj). " result(s)");
+               $self->event(_mk_not_found($type, $arg)) unless @$obj;
+       }
+
+       if( $action eq 'create' ) {
+        my $idf = $obj->Identity;
+               $self->log(I, "created a new $type object with Identity " . $obj->$idf);
+               $arg->$idf($obj->$idf);
+       }
+
+       $self->data($obj); # cache the data for convenience
+
+       return ($obj) ? $obj : 1;
+}
+
+
+sub _mk_not_found {
+       my( $type, $arg ) = @_;
+       (my $t = $type) =~ s/\./_/og;
+       $t = uc($t);
+       return ShareStuff::Event->new("${t}_NOT_FOUND", payload => $arg);
+}
+
+
+
+# utility method for loading
+sub __fm2meth { 
+       my $str = shift;
+       my $sep = shift;
+       $str =~ s/Fieldmapper:://o;
+       $str =~ s/::/$sep/g;
+       return $str;
+}
+
+
+# -------------------------------------------------------------
+# Load up the methods from the FM classes
+# -------------------------------------------------------------
+
+sub init {
+    no warnings;    #  Here we potentially redefine subs via eval
+    my $map = $Fieldmapper::fieldmap;
+    for my $object (keys %$map) {
+        my $obj  = __fm2meth($object, '_');
+        my $type = __fm2meth($object, '.');
+        foreach my $command (qw/ update retrieve search create delete batch_retrieve retrieve_all /) {
+            eval "sub ${command}_$obj {return shift()->runmethod('$command', '$type', \@_);}\n";
+        }
+        # TODO: performance test against concatenating a big string of all the subs and eval'ing only ONCE.
+    }
+}
+
+init();  # Add very many subs to this namespace
+
+sub json_query {
+    my( $self, $arg, $options ) = @_;
+    $options ||= {};
+       my @arg = ( ref($arg) eq 'ARRAY' ) ? @$arg : ($arg);
+    my $method = $self->app.'.json_query.atomic';
+    $method =~ s/\.atomic$//o if $self->substream($$options{substream} || 0);
+
+    $self->timeout($$options{timeout});
+    $self->discard($$options{discard});
+       $self->clear_event;
+    my $obj;
+    my $err;
+    
+    try {
+        $obj = $self->request($method, @arg);
+    } catch Error with { $err = shift; };
+
+    if( $err ) {
+        $self->event(
+            ShareStuff::Event->new( 'DATABASE_QUERY_FAILED',
+            payload => $arg, debug => "$err" ));
+        return undef;
+    }
+
+    $self->log(I, "json_query : returned ".scalar(@$obj). " result(s)") if (ref($obj));
+    return $obj;
+}
+
+
+
+1;
+
+
diff --git a/src/perl/lib/ShareStuff/Const.pm b/src/perl/lib/ShareStuff/Const.pm
new file mode 100644 (file)
index 0000000..f718c53
--- /dev/null
@@ -0,0 +1,133 @@
+package ShareStuff::Const;
+use strict; use warnings;
+use vars qw(@EXPORT_OK %EXPORT_TAGS);
+use Exporter;
+use base qw/Exporter/;
+
+
+# ---------------------------------------------------------------------
+# Shoves defined constants into the export array
+# so they don't have to be listed twice in the code
+# ---------------------------------------------------------------------
+sub econst {
+   my($name, $value) = @_;
+   my $caller = caller;
+   no strict;
+   *{$name} = sub () { $value };
+   push @{$caller.'::EXPORT_OK'}, $name;
+}
+
+# ---------------------------------------------------------------------
+# CONSTANTS
+# ---------------------------------------------------------------------
+
+
+
+# ---------------------------------------------------------------------
+# Copy Statuses
+# ---------------------------------------------------------------------
+econst OILS_COPY_STATUS_AVAILABLE     => 0;
+econst OILS_COPY_STATUS_CHECKED_OUT   => 1;
+econst OILS_COPY_STATUS_BINDERY       => 2;
+econst OILS_COPY_STATUS_LOST          => 3;
+econst OILS_COPY_STATUS_MISSING       => 4;
+econst OILS_COPY_STATUS_IN_PROCESS    => 5;
+econst OILS_COPY_STATUS_IN_TRANSIT    => 6;
+econst OILS_COPY_STATUS_RESHELVING    => 7;
+econst OILS_COPY_STATUS_ON_HOLDS_SHELF=> 8;
+econst OILS_COPY_STATUS_ON_ORDER            => 9;
+econst OILS_COPY_STATUS_ILL           => 10;
+econst OILS_COPY_STATUS_CATALOGING    => 11;
+econst OILS_COPY_STATUS_RESERVES      => 12;
+econst OILS_COPY_STATUS_DISCARD       => 13;
+econst OILS_COPY_STATUS_DAMAGED       => 14;
+econst OILS_COPY_STATUS_ON_RESV_SHELF => 15;
+
+
+# ---------------------------------------------------------------------
+# Circ defaults for pre-cataloged copies
+# ---------------------------------------------------------------------
+econst OILS_PRECAT_COPY_FINE_LEVEL    => 2;
+econst OILS_PRECAT_COPY_LOAN_DURATION => 2;
+econst OILS_PRECAT_CALL_NUMBER        => -1;
+econst OILS_PRECAT_RECORD                           => -1;
+
+
+# ---------------------------------------------------------------------
+# Circ constants
+# ---------------------------------------------------------------------
+econst OILS_CIRC_DURATION_SHORT       => 1;
+econst OILS_CIRC_DURATION_NORMAL      => 2;
+econst OILS_CIRC_DURATION_EXTENDED    => 3;
+econst OILS_REC_FINE_LEVEL_LOW        => 1;
+econst OILS_REC_FINE_LEVEL_NORMAL     => 2;
+econst OILS_REC_FINE_LEVEL_HIGH       => 3;
+econst OILS_STOP_FINES_CHECKIN        => 'CHECKIN';
+econst OILS_STOP_FINES_RENEW          => 'RENEW';
+econst OILS_STOP_FINES_LOST           => 'LOST';
+econst OILS_STOP_FINES_CLAIMSRETURNED => 'CLAIMSRETURNED';
+econst OILS_STOP_FINES_LONGOVERDUE    => 'LONGOVERDUE';
+econst OILS_STOP_FINES_MAX_FINES      => 'MAXFINES';
+econst OILS_STOP_FINES_CLAIMS_NEVERCHECKEDOUT => 'CLAIMSNEVERCHECKEDOUT';
+econst OILS_UNLIMITED_CIRC_DURATION   => 'unlimited';
+
+# ---------------------------------------------------------------------
+# Settings
+# ---------------------------------------------------------------------
+econst OILS_SETTING_LOST_PROCESSING_FEE => 'circ.lost_materials_processing_fee';
+econst OILS_SETTING_DEF_ITEM_PRICE => 'cat.default_item_price';
+econst OILS_SETTING_ORG_BOUNCED_EMAIL => 'org.bounced_emails';
+econst OILS_SETTING_CHARGE_LOST_ON_ZERO => 'circ.charge_lost_on_zero';
+econst OILS_SETTING_VOID_OVERDUE_ON_LOST => 'circ.void_overdue_on_lost';
+econst OILS_SETTING_HOLD_SOFT_STALL => 'circ.hold_stalling.soft';
+econst OILS_SETTING_HOLD_HARD_STALL => 'circ.hold_stalling.hard';
+econst OILS_SETTING_HOLD_SOFT_BOUNDARY => 'circ.hold_boundary.soft';
+econst OILS_SETTING_HOLD_HARD_BOUNDARY => 'circ.hold_boundary.hard';
+econst OILS_SETTING_HOLD_EXPIRE => 'circ.hold_expire_interval';
+econst OILS_SETTING_HOLD_ESIMATE_WAIT_INTERVAL => 'circ.holds.default_estimated_wait_interval';
+econst OILS_SETTING_VOID_LOST_ON_CHECKIN                => 'circ.void_lost_on_checkin';
+econst OILS_SETTING_MAX_ACCEPT_RETURN_OF_LOST           => 'circ.max_accept_return_of_lost';
+econst OILS_SETTING_VOID_LOST_PROCESS_FEE_ON_CHECKIN    => 'circ.void_lost_proc_fee_on_checkin';
+econst OILS_SETTING_RESTORE_OVERDUE_ON_LOST_RETURN      => 'circ.restore_overdue_on_lost_return';
+econst OILS_SETTING_LOST_IMMEDIATELY_AVAILABLE          => 'circ.lost_immediately_available';
+econst OILS_SETTING_BLOCK_HOLD_FOR_EXPIRED_PATRON       => 'circ.holds.expired_patron_block';
+econst OILS_SETTING_GENERATE_OVERDUE_ON_LOST_RETURN     => 'circ.lost.generate_overdue_on_checkin';
+
+
+
+
+econst OILS_HOLD_TYPE_COPY        => 'C';
+econst OILS_HOLD_TYPE_FORCE       => 'F';
+econst OILS_HOLD_TYPE_RECALL      => 'R';
+econst OILS_HOLD_TYPE_ISSUANCE    => 'I';
+econst OILS_HOLD_TYPE_VOLUME      => 'V';
+econst OILS_HOLD_TYPE_TITLE       => 'T';
+econst OILS_HOLD_TYPE_METARECORD  => 'M';
+econst OILS_HOLD_TYPE_MONOPART    => 'P';
+
+
+econst OILS_BILLING_TYPE_OVERDUE_MATERIALS => 'Overdue materials';
+econst OILS_BILLING_TYPE_COLLECTION_FEE => 'Long Overdue Collection Fee';
+econst OILS_BILLING_TYPE_DEPOSIT => 'System: Deposit';
+econst OILS_BILLING_TYPE_RENTAL => 'System: Rental';
+econst OILS_BILLING_NOTE_SYSTEM => 'SYSTEM GENERATED';
+
+econst OILS_ACQ_DEBIT_TYPE_PURCHASE => 'purchase';
+econst OILS_ACQ_DEBIT_TYPE_TRANSFER => 'xfer';
+
+# all penalties with ID < 100 are managed automatically
+econst OILS_PENALTY_AUTO_ID => 100;
+econst OILS_PENALTY_PATRON_EXCEEDS_FINES => 1;
+econst OILS_PENALTY_PATRON_EXCEEDS_OVERDUE_COUNT => 2;
+econst OILS_PENALTY_INVALID_PATRON_ADDRESS => 29;
+
+
+econst OILS_BILLING_TYPE_NOTIFICATION_FEE => 9;
+
+
+
+# ---------------------------------------------------------------------
+# finally, export all the constants
+# ---------------------------------------------------------------------
+%EXPORT_TAGS = ( const => [ @EXPORT_OK ] );
+
diff --git a/src/perl/lib/ShareStuff/Event.pm b/src/perl/lib/ShareStuff/Event.pm
new file mode 100644 (file)
index 0000000..5777eed
--- /dev/null
@@ -0,0 +1,91 @@
+package ShareStuff::Event;
+# vim:noet:ts=4
+use strict; use warnings;
+use XML::LibXML;
+use OpenSRF::Utils::SettingsClient;
+use OpenSRF::Utils::Logger;
+my $logger = "OpenSRF::Utils::Logger";
+
+
+# Returns a new Event data hash (not a blessed object)
+# The first param is the event name
+# Following the first param is an optional hash of params:
+#              perm => the name of the permission error for permimssion errors
+#              permloc => the location of the permission error for permission errors
+#              payload => the payload to be returned on successfull events
+
+
+my $events = undef;
+my $descs  = undef;
+
+sub new {
+    my( $class, $event, %params ) = @_;
+    _load_events() unless $events;
+
+    throw OpenSRF::EX ("Bad event name: $event") unless $event;
+    my $e = $events->{$event};
+    $e = '' unless defined $e;
+
+    my(   $m,   $f,   $l ) = caller(0);
+    my(  $mm,  $ff,  $ll ) = caller(1);
+    my( $mmm, $fff, $lll ) = caller(2);
+
+    $f   ||= "";
+    $l   ||= "";
+    $ff  ||= "";
+    $ll  ||= "";
+    $fff ||= "";
+    $lll ||= "";
+
+    my $lang = 'en-US'; # assume english for now
+
+    my $t = CORE::localtime();
+
+    return { 
+        ilsevent   => $e, 
+        textcode   => $event, 
+        stacktrace => "$f:$l $ff:$ll $fff:$lll", 
+        desc       => $descs->{$lang}->{$e || ''} || '',
+        servertime => $t,
+        pid        => $$, %params
+    };
+}
+
+sub _load_events {
+       my $settings_client = OpenSRF::Utils::SettingsClient->new();
+       my $eventsxml =  $settings_client->config_value( "ils_events" );
+
+       if(!$eventsxml) { 
+               throw OpenSRF::EX ("No ils_events file found in settings config"); 
+       }
+
+       $logger->info("Loading events xml file $eventsxml");
+
+       my $doc = XML::LibXML->new->parse_file($eventsxml);
+
+       my @nodes = $doc->documentElement->findnodes('//event');
+       for my $node (@nodes) {
+               $events->{$node->getAttribute('textcode')} = 
+                       $node->getAttribute('code');
+       }
+
+       $descs = {};
+       my @desc = $doc->documentElement->findnodes('//desc');
+       for my $d (@desc) {
+               my $lang = $d->getAttributeNS('http://www.w3.org/XML/1998/namespace', 'lang');
+               my $code = $d->parentNode->getAttribute('code');
+               unless ($descs && $lang && exists $descs->{$lang}) {
+                       $descs->{$lang} = {};
+                       if (!$descs) {
+                               $logger->error("No error description nodes found in $eventsxml.");
+                       }
+                       if (!$lang) {
+                               $logger->error("No xml:lang attribute found for node in $eventsxml.");
+                       }
+               }
+               $descs->{$lang}->{$code} = $d->textContent;
+       }
+}
+
+
+1;
index ce7c6bf..b7138eb 100644 (file)
@@ -7,7 +7,7 @@ use Template qw( :template );
 use CGI;
 use OpenSRF::EX qw(:try);
 use OpenSRF::System;
-use OpenILS::Utils::CStoreEditor;
+use ShareStuff::CStoreEditor qw/:funcs/;
 
 
 # set the bootstrap config and template include directory when
@@ -37,7 +37,7 @@ sub handler {
 
     $env{authtoken} = $cgi->cookie('ses') || $cgi->param('ses');
     $env{user} = verify_login($env{authtoken});
-    $env{cstore} = new OpenILS::Utils::CStoreEditor;
+    $env{cstore} = new_editor( authtoken => $env{authtoken} );
 
 
     $env{file} = $env{r}->path_info;