add overlimit_groups agent-specific config, overriding export-specific overlimit_grou...
[freeside.git] / FS / FS / svc_acct.pm
index af490d9..fda6e3c 100644 (file)
@@ -16,6 +16,7 @@ use vars qw( @ISA $DEBUG $me $conf $skip_fuzzyfiles
              $radius_password $radius_ip
              $dirhash
              @saltset @pw_set );
+use Math::BigInt;
 use Carp;
 use Fcntl qw(:flock);
 use Date::Format;
@@ -530,48 +531,8 @@ sub insert {
   local $FS::UID::AutoCommit = 0;
   my $dbh = dbh;
 
-  my $error = $self->check;
-  return $error if $error;
-
-  if ( $self->svcnum && qsearchs('cust_svc',{'svcnum'=>$self->svcnum}) ) {
-    my $cust_svc = qsearchs('cust_svc',{'svcnum'=>$self->svcnum});
-    unless ( $cust_svc ) {
-      $dbh->rollback if $oldAutoCommit;
-      return "no cust_svc record found for svcnum ". $self->svcnum;
-    }
-    $self->pkgnum($cust_svc->pkgnum);
-    $self->svcpart($cust_svc->svcpart);
-  }
-
-  $error = $self->_check_duplicate;
-  if ( $error ) {
-    $dbh->rollback if $oldAutoCommit;
-    return $error;
-  }
-
-  # set usage fields and thresholds if unset but set in a package def
-  if ( $self->pkgnum ) {
-    my $cust_pkg = qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
-    my $part_pkg = $cust_pkg->part_pkg if $cust_pkg;
-    if ( $part_pkg && $part_pkg->can('usage_valuehash') ) {
-
-      my %values = $part_pkg->usage_valuehash;
-      my $multiplier = $conf->exists('svc_acct-usage_threshold') 
-                         ? 1 - $conf->config('svc_acct-usage_threshold')/100
-                         : 0.20; #doesn't matter
-
-      foreach ( keys %values ) {
-        next if $self->getfield($_);
-        $self->setfield( $_, $values{$_} );
-        $self->setfield( $_. '_threshold', int( $values{$_} * $multiplier ) )
-          if $conf->exists('svc_acct-usage_threshold');
-      }
-
-    }
-  }
-
   my @jobnums;
-  $error = $self->SUPER::insert(
+  my $error = $self->SUPER::insert(
     'jobnums'       => \@jobnums,
     'child_objects' => $self->child_objects,
     %options,
@@ -681,6 +642,31 @@ sub insert {
   ''; #no error
 }
 
+# set usage fields and thresholds if unset but set in a package def
+sub preinsert_hook_first {
+  my $self = shift;
+
+  return '' unless $self->pkgnum;
+
+  my $cust_pkg = qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
+  my $part_pkg = $cust_pkg->part_pkg if $cust_pkg;
+  return '' unless $part_pkg && $part_pkg->can('usage_valuehash');
+
+  my %values = $part_pkg->usage_valuehash;
+  my $multiplier = $conf->exists('svc_acct-usage_threshold') 
+                     ? 1 - $conf->config('svc_acct-usage_threshold')/100
+                     : 0.20; #doesn't matter
+
+  foreach ( keys %values ) {
+    next if $self->getfield($_);
+    $self->setfield( $_, $values{$_} );
+    $self->setfield( $_. '_threshold', int( $values{$_} * $multiplier ) )
+      if $conf->exists('svc_acct-usage_threshold');
+  }
+
+  ''; #no error
+}
+
 =item delete
 
 Deletes this account from the database.  If there is an error, returns the
@@ -1393,6 +1379,29 @@ sub radius_reply {
     $reply{'Session-Timeout'} = $self->seconds;
   }
 
+  if ( $conf->exists('radius-chillispot-max') ) {
+    #http://dev.coova.org/svn/coova-chilli/doc/dictionary.chillispot
+
+    #hmm.  just because sqlradius.pm says so?
+    my %whatis = (
+      'input'  => 'up',
+      'output' => 'down',
+      'total'  => 'total',
+    );
+
+    foreach my $what (qw( input output total )) {
+      my $is = $whatis{$what}.'bytes';
+      if ( $self->$is() =~ /\d/ ) {
+        my $big = new Math::BigInt $self->$is();
+        $big = new Math::BigInt '0' if $big->is_neg();
+        my $att = "Chillispot-Max-\u$what";
+        $reply{"$att-Octets"}    = $big->copy->band(0xffffffff)->bstr;
+        $reply{"$att-Gigawords"} = $big->copy->brsft(32)->bstr;
+      }
+    }
+
+  }
+
   %reply;
 }
 
@@ -1425,11 +1434,15 @@ sub radius_check {
   my $pw_attrib = length($password) <= 12 ? $radius_password : 'Crypt-Password';  $check{$pw_attrib} = $password;
 
   my $cust_svc = $self->cust_svc;
-  die "FATAL: no cust_svc record for svc_acct.svcnum ". $self->svcnum. "\n"
-    unless $cust_svc;
-  my $cust_pkg = $cust_svc->cust_pkg;
-  if ( $cust_pkg && $cust_pkg->part_pkg->is_prepaid && $cust_pkg->bill ) {
-    $check{'Expiration'} = time2str('%B %e %Y %T', $cust_pkg->bill ); #http://lists.cistron.nl/pipermail/freeradius-users/2005-January/040184.html
+  if ( $cust_svc ) {
+    my $cust_pkg = $cust_svc->cust_pkg;
+    if ( $cust_pkg && $cust_pkg->part_pkg->is_prepaid && $cust_pkg->bill ) {
+      $check{'Expiration'} = time2str('%B %e %Y %T', $cust_pkg->bill ); #http://lists.cistron.nl/pipermail/freeradius-users/2005-January/040184.html
+    }
+  } else {
+    warn "WARNING: no cust_svc record for svc_acct.svcnum ". $self->svcnum.
+         "; can't set Expiration\n"
+      unless $cust_svc;
   }
 
   %check;
@@ -1687,32 +1700,51 @@ sub _op_usage {
   die "Can't update $column for svcnum". $self->svcnum
     if $rv == 0;
 
+  #$self->snapshot; #not necessary, we retain the old values
+  #create an object with the updated usage values
+  my $new = qsearchs('svc_acct', { 'svcnum' => $self->svcnum });
+  #call exports
+  my $error = $new->replace($self);
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return "Error replacing: $error";
+  }
+
+  #overlimit_action eq 'cancel' handling
+  my $cust_pkg = $self->cust_svc->cust_pkg;
+  if ( $cust_pkg
+       && $cust_pkg->part_pkg->option('overlimit_action', 1) eq 'cancel' 
+       && $op eq '-' && &{$op2condition{$op}}($self, $column, $amount)
+     )
+  {
+
+    my $error = $cust_pkg->cancel; #XXX should have a reason
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "Error cancelling: $error";
+    }
+
+    #nothing else is relevant if we're cancelling, so commit & return success
+    warn "$me update successful; committing\n"
+      if $DEBUG;
+    $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+    return '';
+
+  }
+
   my $action = $op2action{$op};
 
   if ( &{$op2condition{$op}}($self, $column, $amount) &&
         ( $action eq 'suspend'   && !$self->overlimit 
        || $action eq 'unsuspend' &&  $self->overlimit ) 
      ) {
-    foreach my $part_export ( $self->cust_svc->part_svc->part_export ) {
-      if ($part_export->option('overlimit_groups')) {
-        my ($new,$old);
-        my $other = new FS::svc_acct $self->hashref;
-        my $groups = &{ $self->_fieldhandlers->{'usergroup'} }
-                       ($self, $part_export->option('overlimit_groups'));
-        $other->usergroup( $groups );
-        if ($action eq 'suspend'){
-          $new = $other; $old = $self;
-        }else{
-          $new = $self; $old = $other;
-        }
-        my $error = $part_export->export_replace($new, $old);
-        $error ||= $self->overlimit($action);
-        if ( $error ) {
-          $dbh->rollback if $oldAutoCommit;
-          return "Error replacing radius groups in export, ${op}: $error";
-        }
-      }
+
+    my $error = $self->_op_overlimit($action);
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
     }
+
   }
 
   if ( $conf->exists("svc_acct-usage_$action")
@@ -1757,6 +1789,61 @@ sub _op_usage {
 
 }
 
+sub _op_overlimit {
+  my( $self, $action ) = @_;
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  my $cust_pkg = $self->cust_svc->cust_pkg;
+
+  my $agent_overlimit =
+    $cust_pkg
+      ? $conf->config('overlimit_groups', $cust_pkg->cust_main->agentnum )
+      : '';
+
+  foreach my $part_export ( $self->cust_svc->part_svc->part_export ) {
+
+    my $groups = $agent_overlimit || $part_export->option('overlimit_groups');
+    next unless $groups;
+
+    my $gref = &{ $self->_fieldhandlers->{'usergroup'} }( $self, $groups );
+
+    my $other = new FS::svc_acct $self->hashref;
+    $other->usergroup( $gref );
+
+    my($new,$old);
+    if ($action eq 'suspend') {
+      $new = $other;
+      $old = $self;
+    } else { # $action eq 'unsuspend'
+      $new = $self;
+      $old = $other;
+    }
+
+    my $error = $part_export->export_replace($new, $old)
+                || $self->overlimit($action);
+
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "Error replacing radius groups: $error";
+    }
+
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+  '';
+
+}
+
 sub set_usage {
   my( $self, $valueref, %options ) = @_;
 
@@ -1818,29 +1905,31 @@ sub set_usage {
       if $rv == 0;
   }
 
+  #$self->snapshot; #not necessary, we retain the old values
+  #create an object with the updated usage values
+  my $new = qsearchs('svc_acct', { 'svcnum' => $self->svcnum });
+  #call exports
+  my $error = $new->replace($self);
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return "Error replacing: $error";
+  }
+
   if ( $reset ) {
-    my $error;
-
-    if ($self->overlimit) {
-      $error = $self->overlimit('unsuspend');
-      foreach my $part_export ( $self->cust_svc->part_svc->part_export ) {
-        if ($part_export->option('overlimit_groups')) {
-          my $old = new FS::svc_acct $self->hashref;
-          my $groups = &{ $self->_fieldhandlers->{'usergroup'} }
-                         ($self, $part_export->option('overlimit_groups'));
-          $old->usergroup( $groups );
-          $error ||= $part_export->export_replace($self, $old);
-        }
-      }
-    }
 
-    if ( $conf->exists("svc_acct-usage_unsuspend")) {
-      $error ||= $self->cust_svc->cust_pkg->unsuspend;
-    }
+    my $error = '';
+
+    $error = $self->_op_overlimit('unsuspend')
+      if $self->overlimit;;
+
+    $error ||= $self->cust_svc->cust_pkg->unsuspend
+      if $conf->exists("svc_acct-usage_unsuspend");
+
     if ( $error ) {
       $dbh->rollback if $oldAutoCommit;
       return "Error unsuspending: $error";
     }
+
   }
 
   warn "$me update successful; committing\n"
@@ -2487,6 +2576,8 @@ probably live somewhere else...
 insertion of RADIUS group stuff in insert could be done with child_objects now
 (would probably clean up export of them too)
 
+_op_usage and set_usage bypass the history... maybe they shouldn't
+
 =head1 SEE ALSO
 
 L<FS::svc_Common>, edit/part_svc.cgi from an installed web interface,