show credit balance on invoices, #11564
[freeside.git] / FS / FS / svc_acct.pm
index 53b9fd6..0525b97 100644 (file)
@@ -1,12 +1,15 @@
 package FS::svc_acct;
 
 use strict;
-use vars qw( @ISA $DEBUG $me $conf $skip_fuzzyfiles
+use base qw( FS::svc_Domain_Mixin FS::svc_CGP_Mixin FS::svc_CGPRule_Mixin
+             FS::svc_Common );
+use vars qw( $DEBUG $me $conf $skip_fuzzyfiles
              $dir_prefix @shells $usernamemin
              $usernamemax $passwordmin $passwordmax
              $username_ampersand $username_letter $username_letterfirst
              $username_noperiod $username_nounderscore $username_nodash
              $username_uppercase $username_percent $username_colon
+             $username_slash $username_equals $username_pound
              $password_noampersand $password_noexclamation
              $warning_template $warning_from $warning_subject $warning_mimetype
              $warning_cc
@@ -32,12 +35,11 @@ use FS::Msgcat qw(gettext);
 use FS::UI::bytecount;
 use FS::UI::Web;
 use FS::part_pkg;
-use FS::svc_Common;
-use FS::cust_svc;
 use FS::part_svc;
 use FS::svc_acct_pop;
 use FS::cust_main_invoice;
 use FS::svc_domain;
+use FS::svc_pbx;
 use FS::raddb;
 use FS::queue;
 use FS::radius_usergroup;
@@ -46,8 +48,7 @@ use FS::part_export;
 use FS::svc_forward;
 use FS::svc_www;
 use FS::cdr;
-
-@ISA = qw( FS::svc_Common );
+use FS::acct_snarf;
 
 $DEBUG = 0;
 $me = '[FS::svc_acct]';
@@ -74,6 +75,9 @@ FS::UID->install_callback( sub {
   $username_ampersand = $conf->exists('username-ampersand');
   $username_percent = $conf->exists('username-percent');
   $username_colon = $conf->exists('username-colon');
+  $username_slash = $conf->exists('username-slash');
+  $username_equals = $conf->exists('username-equals');
+  $username_pound = $conf->exists('username-pound');
   $password_noampersand = $conf->exists('password-noexclamation');
   $password_noexclamation = $conf->exists('password-noexclamation');
   $dirhash = $conf->config('dirhash') || 0;
@@ -101,7 +105,7 @@ FS::UID->install_callback( sub {
 );
 
 @saltset = ( 'a'..'z' , 'A'..'Z' , '0'..'9' , '.' , '/' );
-@pw_set = ( 'a'..'z', 'A'..'Z', '0'..'9', '(', ')', '#', '!', '.', ',' );
+@pw_set = ( 'a'..'z', 'A'..'Z', '0'..'9', '(', ')', '#', '.', ',' );
 
 sub _cache {
   my $self = shift;
@@ -161,45 +165,71 @@ FS::svc_Common.  The following fields are currently supported:
 
 =over 4
 
-=item svcnum - primary key (assigned automatcially for new accounts)
+=item svcnum
+
+Primary key (assigned automatcially for new accounts)
 
 =item username
 
-=item _password - generated if blank
+=item _password
+
+generated if blank
+
+=item _password_encoding
+
+plain, crypt, ldap (or empty for autodetection)
 
-=item _password_encoding - plain, crypt, ldap (or empty for autodetection)
+=item sec_phrase
 
-=item sec_phrase - security phrase
+security phrase
 
-=item popnum - Point of presence (see L<FS::svc_acct_pop>)
+=item popnum
+
+Point of presence (see L<FS::svc_acct_pop>)
 
 =item uid
 
 =item gid
 
-=item finger - GECOS
+=item finger
+
+GECOS
 
-=item dir - set automatically if blank (and uid is not)
+=item dir
+
+set automatically if blank (and uid is not)
 
 =item shell
 
-=item quota - (unimplementd)
+=item quota
 
-=item slipip - IP address
+=item slipip
 
-=item seconds - 
+IP address
 
-=item upbytes - 
+=item seconds
 
-=item downbytes - 
+=item upbytes
 
-=item totalbytes - 
+=item downbyte
 
-=item domsvc - svcnum from svc_domain
+=item totalbytes
 
-=item radius_I<Radius_Attribute> - I<Radius-Attribute> (reply)
+=item domsvc
 
-=item rc_I<Radius_Attribute> - I<Radius-Attribute> (check)
+svcnum from svc_domain
+
+=item pbxsvc
+
+Optional svcnum from svc_pbx
+
+=item radius_I<Radius_Attribute>
+
+I<Radius-Attribute> (reply)
+
+=item rc_I<Radius_Attribute>
+
+I<Radius-Attribute> (check)
 
 =back
 
@@ -244,8 +274,32 @@ sub table_info {
                          disable_fixed => 1,
                          disable_select => 1,
                        },
+        'password_selfchange' => { label => 'Password modification',
+                                   type  => 'checkbox',
+                                 },
+        'password_recover'    => { label => 'Password recovery',
+                                   type  => 'checkbox',
+                                 },
         'quota'     => { 
-                         label => 'Quota',
+                         label => 'Quota', #Mail storage limit
+                         type => 'text',
+                         disable_inventory => 1,
+                         disable_select => 1,
+                       },
+        'file_quota'=> { 
+                         label => 'File storage limit',
+                         type => 'text',
+                         disable_inventory => 1,
+                         disable_select => 1,
+                       },
+        'file_maxnum'=> { 
+                         label => 'Number of files limit',
+                         type => 'text',
+                         disable_inventory => 1,
+                         disable_select => 1,
+                       },
+        'file_maxsize'=> { 
+                         label => 'File size limit',
                          type => 'text',
                          disable_inventory => 1,
                          disable_select => 1,
@@ -273,7 +327,11 @@ sub table_info {
                          select_key   => 'svcnum',
                          select_label => 'domain',
                          disable_inventory => 1,
-
+                       },
+        'pbxsvc'    => { label => 'PBX',
+                         type  => 'select-svc_pbx.html',
+                         disable_inventory => 1,
+                         disable_select => 1, #UI wonky, pry works otherwise
                        },
         'usergroup' => {
                          label => 'RADIUS groups',
@@ -350,6 +408,120 @@ sub table_info {
                                    label     => 'Last logout',
                                    type      => 'disabled',
                                  },
+
+        'cgp_aliases' => { 
+                           label => 'Communigate aliases',
+                           type  => 'text',
+                           disable_inventory => 1,
+                           disable_select    => 1,
+                         },
+        #settings
+        'cgp_type'=> { 
+                       label => 'Communigate account type',
+                       type => 'select',
+                       select_list => [qw( MultiMailbox TextMailbox MailDirMailbox AGrade BGrade CGrade )],
+                       disable_inventory => 1,
+                       disable_select    => 1,
+                     },
+        'cgp_accessmodes' => { 
+                               label => 'Communigate enabled services',
+                               type  => 'communigate_pro-accessmodes',
+                               disable_inventory => 1,
+                               disable_select    => 1,
+                             },
+        'cgp_rulesallowed'   => {
+          label       => 'Allowed mail rules',
+          type        => 'select',
+          select_list => [ '', 'No', 'Filter Only', 'All But Exec', 'Any' ],
+          disable_inventory => 1,
+          disable_select    => 1,
+        },
+        'cgp_rpopallowed'    => { label => 'RPOP modifications',
+                                  type  => 'checkbox',
+                                },
+        'cgp_mailtoall'      => { label => 'Accepts mail to "all"',
+                                  type  => 'checkbox',
+                                },
+        'cgp_addmailtrailer' => { label => 'Add trailer to sent mail',
+                                  type  => 'checkbox',
+                                },
+        'cgp_archiveafter'   => {
+          label       => 'Archive messages after',
+          type        => 'select',
+          select_hash => [ 
+                           -2 => 'default(730 days)',
+                           0 => 'Never',
+                           86400 => '24 hours',
+                           172800 => '2 days',
+                           259200 => '3 days',
+                           432000 => '5 days',
+                           604800 => '7 days',
+                           1209600 => '2 weeks',
+                           2592000 => '30 days',
+                           7776000 => '90 days',
+                           15552000 => '180 days',
+                           31536000 => '365 days',
+                           63072000 => '730 days',
+                         ],
+          disable_inventory => 1,
+          disable_select    => 1,
+        },
+        #XXX mailing lists
+
+        #preferences
+        'cgp_deletemode' => { 
+                              label => 'Communigate message delete method',
+                              type  => 'select',
+                              select_list => [ 'Move To Trash', 'Immediately', 'Mark' ],
+                              disable_inventory => 1,
+                              disable_select    => 1,
+                            },
+        'cgp_emptytrash' => { 
+                              label     => 'Communigate on logout remove trash',
+                              type        => 'select',
+                              select_list => __PACKAGE__->cgp_emptytrash_values,
+                              disable_inventory => 1,
+                              disable_select    => 1,
+                            },
+        'cgp_language' => {
+                            label => 'Communigate language',
+                            type  => 'select',
+                            select_list => [ '', qw( English Arabic Chinese Dutch French German Hebrew Italian Japanese Portuguese Russian Slovak Spanish Thai ) ],
+                            disable_inventory => 1,
+                            disable_select    => 1,
+                          },
+        'cgp_timezone' => {
+                            label       => 'Communigate time zone',
+                            type        => 'select',
+                            select_list => __PACKAGE__->cgp_timezone_values,
+                            disable_inventory => 1,
+                            disable_select    => 1,
+                          },
+        'cgp_skinname' => {
+                            label => 'Communigate layout',
+                            type  => 'select',
+                            select_list => [ '', '***', 'GoldFleece', 'Skin2' ],
+                            disable_inventory => 1,
+                            disable_select    => 1,
+                          },
+        'cgp_prontoskinname' => {
+                            label => 'Communigate Pronto style',
+                            type  => 'select',
+                            select_list => [ '', 'Pronto', 'Pronto-darkflame', 'Pronto-steel', 'Pronto-twilight', ],
+                            disable_inventory => 1,
+                            disable_select    => 1,
+                          },
+        'cgp_sendmdnmode' => {
+          label => 'Communigate send read receipts',
+          type  => 'select',
+          select_list => [ '', 'Never', 'Manually', 'Automatically' ],
+          disable_inventory => 1,
+          disable_select    => 1,
+        },
+
+        #mail
+        #XXX RPOP settings
+
     },
   };
 }
@@ -436,13 +608,7 @@ sub search_sql {
       $class->search_sql_field('username', $string ).
     ' ) ';
   } else {
-    ' ( '.
-      $class->search_sql_field('username', $string).
-      ( $string =~ /^\d+$/
-          ? 'OR '. $class->search_sql_field('svcnum', $string)
-          : ''
-      ).
-    ' ) ';
+    $class->search_sql_field('username', $string);
   }
 }
 
@@ -580,95 +746,106 @@ sub insert {
     }
 
     #welcome email
-    my ($to,$welcome_template,$welcome_from,$welcome_subject,$welcome_subject_template,$welcome_mimetype)
-      = ('','','','','','');
-
-    if ( $conf->exists('welcome_email', $agentnum) ) {
-      $welcome_template = new Text::Template (
-        TYPE   => 'ARRAY',
-        SOURCE => [ map "$_\n", $conf->config('welcome_email', $agentnum) ]
-      ) or warn "can't create welcome email template: $Text::Template::ERROR";
-      $welcome_from = $conf->config('welcome_email-from', $agentnum);
-        # || 'your-isp-is-dum'
-      $welcome_subject = $conf->config('welcome_email-subject', $agentnum)
-        || 'Welcome';
-      $welcome_subject_template = new Text::Template (
-        TYPE   => 'STRING',
-        SOURCE => $welcome_subject,
-      ) or warn "can't create welcome email subject template: $Text::Template::ERROR";
-      $welcome_mimetype = $conf->config('welcome_email-mimetype', $agentnum)
-        || 'text/plain';
+    my $error = '';
+    my $msgnum = $conf->config('welcome_msgnum', $agentnum);
+    if ( $msgnum ) {
+      my $msg_template = qsearchs('msg_template', { msgnum => $msgnum });
+      $error = $msg_template->send('cust_main' => $cust_main,
+                                   'object'    => $self);
     }
-    if ( $welcome_template && $cust_pkg ) {
-      my $to = join(', ', grep { $_ !~ /^(POST|FAX)$/ } $cust_main->invoicing_list );
-      if ( $to ) {
-
-        my %hash = (
-                     'custnum'  => $self->custnum,
-                     'username' => $self->username,
-                     'password' => $self->_password,
-                     'first'    => $cust_main->first,
-                     'last'     => $cust_main->getfield('last'),
-                     'pkg'      => $cust_pkg->part_pkg->pkg,
-                   );
-        my $wqueue = new FS::queue {
-          'svcnum' => $self->svcnum,
-          'job'    => 'FS::svc_acct::send_email'
-        };
-        my $error = $wqueue->insert(
-          'to'       => $to,
-          'from'     => $welcome_from,
-          'subject'  => $welcome_subject_template->fill_in( HASH => \%hash, ),
-          'mimetype' => $welcome_mimetype,
-          'body'     => $welcome_template->fill_in( HASH => \%hash, ),
-        );
-        if ( $error ) {
-          $dbh->rollback if $oldAutoCommit;
-          return "error queuing welcome email: $error";
-        }
+    else { #!$msgnum
+      my ($to,$welcome_template,$welcome_from,$welcome_subject,$welcome_subject_template,$welcome_mimetype)
+        = ('','','','','','');
+
+      if ( $conf->exists('welcome_email', $agentnum) ) {
+        $welcome_template = new Text::Template (
+          TYPE   => 'ARRAY',
+          SOURCE => [ map "$_\n", $conf->config('welcome_email', $agentnum) ]
+        ) or warn "can't create welcome email template: $Text::Template::ERROR";
+        $welcome_from = $conf->config('welcome_email-from', $agentnum);
+          # || 'your-isp-is-dum'
+        $welcome_subject = $conf->config('welcome_email-subject', $agentnum)
+          || 'Welcome';
+        $welcome_subject_template = new Text::Template (
+          TYPE   => 'STRING',
+          SOURCE => $welcome_subject,
+        ) or warn "can't create welcome email subject template: $Text::Template::ERROR";
+        $welcome_mimetype = $conf->config('welcome_email-mimetype', $agentnum)
+          || 'text/plain';
+      }
+      if ( $welcome_template ) {
+        my $to = join(', ', grep { $_ !~ /^(POST|FAX)$/ } $cust_main->invoicing_list );
+        if ( $to ) {
+
+          my %hash = (
+                       'custnum'  => $self->custnum,
+                       'username' => $self->username,
+                       'password' => $self->_password,
+                       'first'    => $cust_main->first,
+                       'last'     => $cust_main->getfield('last'),
+                       'pkg'      => $cust_pkg->part_pkg->pkg,
+                     );
+          my $wqueue = new FS::queue {
+            'svcnum' => $self->svcnum,
+            'job'    => 'FS::svc_acct::send_email'
+          };
+          my $error = $wqueue->insert(
+            'to'       => $to,
+            'from'     => $welcome_from,
+            'subject'  => $welcome_subject_template->fill_in( HASH => \%hash, ),
+            'mimetype' => $welcome_mimetype,
+            'body'     => $welcome_template->fill_in( HASH => \%hash, ),
+          );
+          if ( $error ) {
+            $dbh->rollback if $oldAutoCommit;
+            return "error queuing welcome email: $error";
+          }
 
-        if ( $options{'depend_jobnum'} ) {
-          warn "$me depend_jobnum found; adding to welcome email dependancies"
-            if $DEBUG;
-          if ( ref($options{'depend_jobnum'}) ) {
-            warn "$me adding jobs ". join(', ', @{$options{'depend_jobnum'}} ).
-                 "to welcome email dependancies"
+          if ( $options{'depend_jobnum'} ) {
+            warn "$me depend_jobnum found; adding to welcome email dependancies"
               if $DEBUG;
-            push @jobnums, @{ $options{'depend_jobnum'} };
-          } else {
-            warn "$me adding job $options{'depend_jobnum'} ".
-                 "to welcome email dependancies"
-              if $DEBUG;
-            push @jobnums, $options{'depend_jobnum'};
+            if ( ref($options{'depend_jobnum'}) ) {
+              warn "$me adding jobs ". join(', ', @{$options{'depend_jobnum'}} ).
+                   "to welcome email dependancies"
+                if $DEBUG;
+              push @jobnums, @{ $options{'depend_jobnum'} };
+            } else {
+              warn "$me adding job $options{'depend_jobnum'} ".
+                   "to welcome email dependancies"
+                if $DEBUG;
+              push @jobnums, $options{'depend_jobnum'};
+            }
           }
-        }
 
-        foreach my $jobnum ( @jobnums ) {
-          my $error = $wqueue->depend_insert($jobnum);
-          if ( $error ) {
-            $dbh->rollback if $oldAutoCommit;
-            return "error queuing welcome email job dependancy: $error";
+          foreach my $jobnum ( @jobnums ) {
+            my $error = $wqueue->depend_insert($jobnum);
+            if ( $error ) {
+              $dbh->rollback if $oldAutoCommit;
+              return "error queuing welcome email job dependancy: $error";
+            }
           }
-        }
-
-      }
 
-    }
+        }
 
-  } # if ( $cust_pkg )
+      } # if $welcome_template
+    } # if !$msgnum
+  } # if $cust_pkg
 
   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
   ''; #no error
 }
 
 # set usage fields and thresholds if unset but set in a package def
+# AND the package already has a last bill date (otherwise they get double added)
 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 $cust_pkg && $cust_pkg->last_bill;
+
+  my $part_pkg = $cust_pkg->part_pkg;
   return '' unless $part_pkg && $part_pkg->can('usage_valuehash');
 
   my %values = $part_pkg->usage_valuehash;
@@ -1017,15 +1194,40 @@ sub check {
 
   my $error = $self->ut_numbern('svcnum')
               #|| $self->ut_number('domsvc')
-              || $self->ut_foreign_key('domsvc', 'svc_domain', 'svcnum' )
+              || $self->ut_foreign_key( 'domsvc', 'svc_domain', 'svcnum' )
+              || $self->ut_foreign_keyn('pbxsvc', 'svc_pbx',    'svcnum' )
               || $self->ut_textn('sec_phrase')
               || $self->ut_snumbern('seconds')
               || $self->ut_snumbern('upbytes')
               || $self->ut_snumbern('downbytes')
               || $self->ut_snumbern('totalbytes')
-              || $self->ut_enum( '_password_encoding',
-                                 [ '', qw( plain crypt ldap ) ]
-                               )
+              || $self->ut_snumbern('seconds_threshold')
+              || $self->ut_snumbern('upbytes_threshold')
+              || $self->ut_snumbern('downbytes_threshold')
+              || $self->ut_snumbern('totalbytes_threshold')
+              || $self->ut_enum('_password_encoding', ['',qw(plain crypt ldap)])
+              || $self->ut_enum('password_selfchange', [ '', 'Y' ])
+              || $self->ut_enum('password_recover',    [ '', 'Y' ])
+              #cardfortress
+              || $self->ut_anything('cf_privatekey')
+              #communigate
+              || $self->ut_textn('cgp_accessmodes')
+              || $self->ut_alphan('cgp_type')
+              || $self->ut_textn('cgp_aliases' ) #well
+              # settings
+              || $self->ut_alphasn('cgp_rulesallowed')
+              || $self->ut_enum('cgp_rpopallowed', [ '', 'Y' ])
+              || $self->ut_enum('cgp_mailtoall', [ '', 'Y' ])
+              || $self->ut_enum('cgp_addmailtrailer', [ '', 'Y' ])
+              || $self->ut_snumbern('cgp_archiveafter')
+              # preferences
+              || $self->ut_alphasn('cgp_deletemode')
+              || $self->ut_enum('cgp_emptytrash', $self->cgp_emptytrash_values)
+              || $self->ut_alphan('cgp_language')
+              || $self->ut_textn('cgp_timezone')
+              || $self->ut_textn('cgp_skinname')
+              || $self->ut_textn('cgp_prontoskinname')
+              || $self->ut_alphan('cgp_sendmdnmode')
   ;
   return $error if $error;
 
@@ -1045,16 +1247,14 @@ sub check {
   }
 
   my $ulen = $usernamemax || $self->dbdef_table->column('username')->length;
-  if ( $username_uppercase ) {
-    $recref->{username} =~ /^([a-z0-9_\-\.\&\%\:]{$usernamemin,$ulen})$/i
-      or return gettext('illegal_username'). " ($usernamemin-$ulen): ". $recref->{username};
-    $recref->{username} = $1;
-  } else {
-    $recref->{username} =~ /^([a-z0-9_\-\.\&\%\:]{$usernamemin,$ulen})$/
-      or return gettext('illegal_username'). " ($usernamemin-$ulen): ". $recref->{username};
-    $recref->{username} = $1;
-  }
 
+  $recref->{username} =~ /^([a-z0-9_\-\.\&\%\:\/\=\#]{$usernamemin,$ulen})$/i
+    or return gettext('illegal_username'). " ($usernamemin-$ulen): ". $recref->{username};
+  $recref->{username} = $1;
+
+  unless ( $username_uppercase ) {
+    $recref->{username} =~ /[A-Z]/ and return gettext('illegal_username');
+  }
   if ( $username_letterfirst ) {
     $recref->{username} =~ /^[a-z]/ or return gettext('illegal_username');
   } elsif ( $username_letter ) {
@@ -1078,6 +1278,16 @@ sub check {
   unless ( $username_colon ) {
     $recref->{username} =~ /\:/ and return gettext('illegal_username');
   }
+  unless ( $username_slash ) {
+    $recref->{username} =~ /\// and return gettext('illegal_username');
+  }
+  unless ( $username_equals ) {
+    $recref->{username} =~ /\=/ and return gettext('illegal_username');
+  }
+  unless ( $username_pound ) {
+    $recref->{username} =~ /\#/ and return gettext('illegal_username');
+  }
+
 
   $recref->{popnum} =~ /^(\d*)$/ or return "Illegal popnum: ".$recref->{popnum};
   $recref->{popnum} = $1;
@@ -1121,7 +1331,7 @@ sub check {
 
   unless ( $part_svc->part_svc_column('dir')->columnflag eq 'F' ) {
 
-    $recref->{dir} =~ /^([\/\w\-\.\&]*)$/
+    $recref->{dir} =~ /^([\/\w\-\.\&\:\#]*)$/
       or return "Illegal directory: ". $recref->{dir};
     $recref->{dir} = $1;
     return "Illegal directory"
@@ -1157,12 +1367,16 @@ sub check {
     }
   }
   $self->getfield('finger') =~
-    /^([\w \t\!\@\#\$\%\&\(\)\-\+\;\'\"\,\.\?\/\*\<\>]*)$/
+    /^([µ_0123456789aAáÁàÀâÂåÅäÄãêæÆbBcCçÇdDðÐeEéÉèÈêÊëËfFgGhHiIíÍìÌîÎïÏjJkKlLmMnNñÑoOóÓòÒôÔöÖõÕøغpPqQrRsSßtTuUúÚùÙûÛüÜvVwWxXyYýÝÿzZþÞ \t\!\@\#\$\%\&\(\)\-\+\;\'\"\,\.\?\/\*\<\>]*)$/
       or return "Illegal finger: ". $self->getfield('finger');
   $self->setfield('finger', $1);
 
-  $recref->{quota} =~ /^(\w*)$/ or return "Illegal quota";
-  $recref->{quota} = $1;
+  for (qw( quota file_quota file_maxsize )) {
+    $recref->{$_} =~ /^(\w*)$/ or return "Illegal $_";
+    $recref->{$_} = $1;
+  }
+  $recref->{file_maxnum} =~ /^\s*(\d*)\s*$/ or return "Illegal file_maxnum";
+  $recref->{file_maxnum} = $1;
 
   unless ( $part_svc->part_svc_column('slipip')->columnflag eq 'F' ) {
     if ( $recref->{slipip} eq '' ) {
@@ -1184,13 +1398,14 @@ sub check {
 
   # First, if _password is blank, generate one and set default encoding.
   if ( ! $recref->{_password} ) {
-    $self->set_password('');
+    $error = $self->set_password('');
   }
   # But if there's a _password but no encoding, assume it's plaintext and 
   # set it to default encoding.
   elsif ( ! $recref->{_password_encoding} ) {
-    $self->set_password($recref->{_password});
+    $error = $self->set_password($recref->{_password});
   }
+  return $error if $error;
 
   # Next, check _password to ensure compliance with the encoding.
   if ( $recref->{_password_encoding} eq 'ldap' ) {
@@ -1232,11 +1447,8 @@ sub check {
       $recref->{_password} =~ /\!/ and return gettext('illegal_password');
     }
   }
-  elsif ( $recref->{_password_encoding} eq 'legacy' ) {
-    # this happens when set_password fails
-    return gettext('illegal_password'). " $passwordmin-$passwordmax ".
-           FS::Msgcat::_gettext('illegal_password_characters').
-           ": ". $recref->{_password};
+  else {
+    return "invalid password encoding ('".$recref->{_password_encoding}."'";
   }
   $self->SUPER::check;
 
@@ -1297,73 +1509,81 @@ is >0), one will be generated randomly.
 =cut
 
 sub set_password {
-  my $self = shift;
-  my $pass = shift;
-  my ($encoding, $encryption);
+  my( $self, $pass ) = ( shift, shift );
+
+  warn "[$me] set_password (to $pass) called on $self: ". Dumper($self)
+     if $DEBUG;
 
+  my $failure = gettext('illegal_password'). " $passwordmin-$passwordmax ".
+                FS::Msgcat::_gettext('illegal_password_characters').
+                ": ". $pass;
 
-  if($self->_password_encoding) {
+  my( $encoding, $encryption ) = ('', '');
+
+  if ( $self->_password_encoding ) {
     $encoding = $self->_password_encoding;
     # identify existing encryption method, try to use it.
     $encryption = $self->_password_encryption;
-    if(!$encryption) {
+    if (!$encryption) {
       # use the system default
       undef $encoding;
     }
   }
 
-  if(!$encoding) {
+  if ( !$encoding ) {
     # set encoding to system default
-    ($encoding, $encryption) = split(/-/, lc($conf->config('default-password-encoding')));
+    ($encoding, $encryption) =
+      split(/-/, lc($conf->config('default-password-encoding')));
     $encoding ||= 'legacy';
     $self->_password_encoding($encoding);
   }
 
-  if($encoding eq 'legacy') {
+  if ( $encoding eq 'legacy' ) {
+
     # The legacy behavior from check():
     # If the password is blank, randomize it and set encoding to 'plain'.
     if(!defined($pass) or (length($pass) == 0 and $passwordmin)) {
       $pass = join('',map($pw_set[ int(rand $#pw_set) ], (0..7) ) );
       $self->_password_encoding('plain');
-    }
-    else {
+    } else {
       # Prefix + valid-length password
       if ( $pass =~ /^((\*SUSPENDED\* |!!?)?)([^\t\n]{$passwordmin,$passwordmax})$/ ) {
         $pass = $1.$3;
         $self->_password_encoding('plain');
-      }
       # Prefix + crypt string
-      elsif ( $pass =~ /^((\*SUSPENDED\* |!!?)?)([\w\.\/\$\;\+]{13,64})$/ ) {
+      elsif ( $pass =~ /^((\*SUSPENDED\* |!!?)?)([\w\.\/\$\;\+]{13,64})$/ ) {
         $pass = $1.$3;
         $self->_password_encoding('crypt');
-      }
       # Various disabled crypt passwords
-      elsif ( $pass eq '*' or
-              $pass eq '!' or
-              $pass eq '!!' ) {
+      } elsif ( $pass eq '*' || $pass eq '!' || $pass eq '!!' ) {
         $self->_password_encoding('crypt');
+      } else {
+        return $failure;
       }
-      else {
-        # do nothing; check() will recognize this as an error
-      }
-   }
+    }
+
+    $self->_password($pass);
+    return;
+
   }
-  elsif($encoding eq 'crypt') {
-    if($encryption eq 'md5') {
+
+  return $failure
+    if $passwordmin && length($pass) < $passwordmin
+    or $passwordmax && length($pass) > $passwordmax;
+
+  if ( $encoding eq 'crypt' ) {
+    if ($encryption eq 'md5') {
       $pass = unix_md5_crypt($pass);
-    }
-    elsif($encryption eq 'des') {
+    } elsif ($encryption eq 'des') {
       $pass = crypt($pass, $saltset[int(rand(64))].$saltset[int(rand(64))]);
     }
-  }
-  elsif($encoding eq 'ldap') {
-    if($encryption eq 'md5') {
+
+  } elsif ( $encoding eq 'ldap' ) {
+    if ($encryption eq 'md5') {
       $pass = md5_base64($pass);
-    }
-    elsif($encryption eq 'sha1') {
+    } elsif ($encryption eq 'sha1') {
       $pass = sha1_base64($pass);
-    }
-    elsif($encryption eq 'crypt') {
+    } elsif ($encryption eq 'crypt') {
       $pass = crypt($pass, $saltset[int(rand(64))].$saltset[int(rand(64))]);
     }
     # else $encryption eq 'plain', do nothing
@@ -1631,30 +1851,20 @@ for the password.
 sub radius_password {
   my $self = shift;
 
-  my($pw_attrib, $password);
+  my $pw_attrib;
   if ( $self->_password_encoding eq 'ldap' ) {
-
     $pw_attrib = 'Password-With-Header';
-    $password = $self->_password;
-
   } elsif ( $self->_password_encoding eq 'crypt' ) {
-
     $pw_attrib = 'Crypt-Password';
-    $password = $self->_password;
-
   } elsif ( $self->_password_encoding eq 'plain' ) {
-
-    $pw_attrib = $radius_password; #Cleartext-Password?  man rlm_pap
-    $password = $self->_password;
-
+    $pw_attrib = $radius_password;
   } else {
-
-    $pw_attrib = length($password) <= 12 ? $radius_password : 'Crypt-Password';
-    $password = $self->_password;
-
+    $pw_attrib = length($self->_password) <= 12
+                   ? $radius_password
+                   : 'Crypt-Password';
   }
 
-  ($pw_attrib, $password);
+  ($pw_attrib, $self->_password);
 
 }
 
@@ -1710,22 +1920,6 @@ sub domain {
   $svc_domain->domain;
 }
 
-=item svc_domain
-
-Returns the FS::svc_domain record for this account's domain (see
-L<FS::svc_domain>).
-
-=cut
-
-# FS::h_svc_acct has a history-aware svc_domain override
-
-sub svc_domain {
-  my $self = shift;
-  $self->{'_domsvc'}
-    ? $self->{'_domsvc'}
-    : qsearchs( 'svc_domain', { 'svcnum' => $self->domsvc } );
-}
-
 =item cust_svc
 
 Returns the FS::cust_svc record for this account (see L<FS::cust_svc>).
@@ -1751,17 +1945,27 @@ sub email {
 =item acct_snarf
 
 Returns an array of FS::acct_snarf records associated with the account.
-If the acct_snarf table does not exist or there are no associated records,
-an empty list is returned
 
 =cut
 
 sub acct_snarf {
   my $self = shift;
-  return () unless dbdef->table('acct_snarf');
-  eval "use FS::acct_snarf;";
-  die $@ if $@;
-  qsearch('acct_snarf', { 'svcnum' => $self->svcnum } );
+  qsearch({
+    'table'    => 'acct_snarf',
+    'hashref'  => { 'svcnum' => $self->svcnum },
+    #'order_by' => 'ORDER BY priority ASC',
+  });
+}
+
+=item cgp_rpop_hashref
+
+Returns an arrayref of RPOP data suitable for Communigate Pro API commands.
+
+=cut
+
+sub cgp_rpop_hashref {
+  my $self = shift;
+  { map { $_->snarfname => $_->cgp_hashref } $self->acct_snarf };
 }
 
 =item decrement_upbytes OCTETS
@@ -2014,14 +2218,14 @@ sub _op_overlimit {
 
   my $cust_pkg = $self->cust_svc->cust_pkg;
 
-  my $agent_overlimit =
+  my $conf_overlimit =
     $cust_pkg
       ? $conf->config('overlimit_groups', $cust_pkg->cust_main->agentnum )
-      : '';
+      : $conf->config('overlimit_groups');
 
   foreach my $part_export ( $self->cust_svc->part_svc->part_export ) {
 
-    my $groups = $agent_overlimit || $part_export->option('overlimit_groups');
+    my $groups = $conf_overlimit || $part_export->option('overlimit_groups');
     next unless $groups;
 
     my $gref = &{ $self->_fieldhandlers->{'usergroup'} }( $self, $groups );
@@ -2076,7 +2280,7 @@ sub set_usage {
   my $reset = 0;
   my %handyhash = ();
   if ( $options{null} ) { 
-    %handyhash = ( map { ( $_ => 'NULL', $_."_threshold" => 'NULL' ) }
+    %handyhash = ( map { ( $_ => undef, $_."_threshold" => undef ) }
                    qw( seconds upbytes downbytes totalbytes )
                  );
   }
@@ -2098,7 +2302,7 @@ sub set_usage {
   #die $error if $error;         #services not explicity changed via the UI
 
   my $sql = "UPDATE svc_acct SET " .
-    join (',', map { "$_ =  $handyhash{$_}" } (keys %handyhash) ).
+    join (',', map { "$_ =  ?" } (keys %handyhash) ).
     " WHERE svcnum = ". $self->svcnum;
 
   warn "$me $sql\n"
@@ -2107,7 +2311,7 @@ sub set_usage {
   if (scalar(keys %handyhash)) {
     my $sth = $dbh->prepare( $sql )
       or die "Error preparing $sql: ". $dbh->errstr;
-    my $rv = $sth->execute();
+    my $rv = $sth->execute(values %handyhash);
     die "Error executing $sql: ". $sth->errstr
       unless defined($rv);
     die "Can't update usage for svcnum ". $self->svcnum
@@ -2117,8 +2321,8 @@ sub set_usage {
   #$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);
+  local($FS::Record::nowarn_identical) = 1;
+  my $error = $new->replace($self); #call exports
   if ( $error ) {
     $dbh->rollback if $oldAutoCommit;
     return "Error replacing: $error";
@@ -3047,61 +3251,4 @@ schema.html from the base documentation.
 
 =cut
 
-=item domain_select_hash %OPTIONS
-
-Returns a hash SVCNUM => DOMAIN ...  representing the domains this customer
-may at present purchase.
-
-Currently available options are: I<pkgnum> I<svcpart>
-
-=cut
-
-sub domain_select_hash {
-  my ($self, %options) = @_;
-  my %domains = ();
-  my $part_svc;
-  my $cust_pkg;
-
-  if (ref($self)) {
-    $part_svc = $self->part_svc;
-    $cust_pkg = $self->cust_svc->cust_pkg
-      if $self->cust_svc;
-  }
-
-  $part_svc = qsearchs('part_svc', { 'svcpart' => $options{svcpart} })
-    if $options{'svcpart'};
-
-  $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $options{pkgnum} })
-    if $options{'pkgnum'};
-
-  if ($part_svc && ( $part_svc->part_svc_column('domsvc')->columnflag eq 'S'
-                  || $part_svc->part_svc_column('domsvc')->columnflag eq 'F')) {
-    %domains = map { $_->svcnum => $_->domain }
-               map { qsearchs('svc_domain', { 'svcnum' => $_ }) }
-               split(',', $part_svc->part_svc_column('domsvc')->columnvalue);
-  }elsif ($cust_pkg && !$conf->exists('svc_acct-alldomains') ) {
-    %domains = map { $_->svcnum => $_->domain }
-               map { qsearchs('svc_domain', { 'svcnum' => $_->svcnum }) }
-               map { qsearch('cust_svc', { 'pkgnum' => $_->pkgnum } ) }
-               qsearch('cust_pkg', { 'custnum' => $cust_pkg->custnum });
-  }else{
-    %domains = map { $_->svcnum => $_->domain } qsearch('svc_domain', {} );
-  }
-
-  if ($part_svc && $part_svc->part_svc_column('domsvc')->columnflag eq 'D') {
-    my $svc_domain = qsearchs('svc_domain',
-      { 'svcnum' => $part_svc->part_svc_column('domsvc')->columnvalue } );
-    if ( $svc_domain ) {
-      $domains{$svc_domain->svcnum}  = $svc_domain->domain;
-    }else{
-      warn "unknown svc_domain.svcnum for part_svc_column domsvc: ".
-           $part_svc->part_svc_column('domsvc')->columnvalue;
-
-    }
-  }
-
-  (%domains);
-}
-
 1;
-