+=item decrement_seconds SECONDS
+
+Decrements the I<seconds> field of this record by the given amount. If there
+is an error, returns the error, otherwise returns false.
+
+=cut
+
+sub decrement_seconds {
+ shift->_op_usage('-', 'seconds', @_);
+}
+
+=item increment_seconds SECONDS
+
+Increments the I<seconds> field of this record by the given amount. If there
+is an error, returns the error, otherwise returns false.
+
+=cut
+
+sub increment_seconds {
+ shift->_op_usage('+', 'seconds', @_);
+}
+
+
+my %op2action = (
+ '-' => 'suspend',
+ '+' => 'unsuspend',
+);
+my %op2condition = (
+ '-' => sub { my($self, $column, $amount) = @_;
+ $self->$column - $amount <= 0;
+ },
+ '+' => sub { my($self, $column, $amount) = @_;
+ ($self->$column || 0) + $amount > 0;
+ },
+);
+my %op2warncondition = (
+ '-' => sub { my($self, $column, $amount) = @_;
+ my $threshold = $column . '_threshold';
+ $self->$column - $amount <= $self->$threshold + 0;
+ },
+ '+' => sub { my($self, $column, $amount) = @_;
+ ($self->$column || 0) + $amount > 0;
+ },
+);
+
+sub _op_usage {
+ my( $self, $op, $column, $amount ) = @_;
+
+ warn "$me _op_usage called for $column on svcnum ". $self->svcnum.
+ ' ('. $self->email. "): $op $amount\n"
+ if $DEBUG;
+
+ return '' unless $amount;
+
+ return ''
+ if $self->cust_svc->part_svc->part_svc_column($column)->columnflag eq 'F';
+
+ 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 $sql = "UPDATE svc_acct SET $column = ".
+ " CASE WHEN $column IS NULL THEN 0 ELSE $column END ". #$column||0
+ " $op ? WHERE svcnum = ?";
+ warn "$me $sql\n"
+ if $DEBUG;
+
+ my $sth = $dbh->prepare( $sql )
+ or die "Error preparing $sql: ". $dbh->errstr;
+ my $rv = $sth->execute($amount, $self->svcnum);
+ die "Error executing $sql: ". $sth->errstr
+ unless defined($rv);
+ die "Can't update $column for svcnum". $self->svcnum
+ if $rv == 0;
+
+ if ( $conf->exists('radius-chillispot-max')
+ || scalar($conf->config('support_packages'))
+ )
+ {
+ #$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 )
+ ) {
+
+ my $error = $self->_op_overlimit($action);
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+
+ }
+
+ if ( $conf->exists("svc_acct-usage_$action")
+ && &{$op2condition{$op}}($self, $column, $amount) ) {
+ #my $error = $self->$action();
+ my $error = $self->cust_svc->cust_pkg->$action();
+ # $error ||= $self->overlimit($action);
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "Error ${action}ing: $error";
+ }
+ }
+
+ if ($warning_msgnum && &{$op2warncondition{$op}}($self, $column, $amount)) {
+ my $wqueue = new FS::queue {
+ 'svcnum' => $self->svcnum,
+ 'job' => 'FS::svc_acct::reached_threshold',
+ };
+
+ # x_threshold race
+ my $error = $wqueue->insert(
+ 'svcnum' => $self->svcnum,
+ 'op' => $op,
+ 'column' => $column
+ );
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "Error queuing threshold activity: $error";
+ }
+ }
+
+ warn "$me update successful; committing\n"
+ if $DEBUG;
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+ '';
+
+}
+
+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 @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 = scalar(@conf_overlimit) ? @conf_overlimit
+ : split(' ',$part_export->option('overlimit_groups'));
+ next unless scalar(@groups);
+
+ my $other = new FS::svc_acct $self->hashref;
+ $other->usergroup(\@groups);
+
+ 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 ) = @_;
+
+ warn "$me set_usage called for svcnum ". $self->svcnum.
+ ' ('. $self->email. "): ".
+ join(', ', map { "$_ => " . $valueref->{$_}} keys %$valueref) . "\n"
+ if $DEBUG;
+
+ local $SIG{HUP} = 'IGNORE';
+ local $SIG{INT} = 'IGNORE';
+ local $SIG{QUIT} = 'IGNORE';
+ local $SIG{TERM} = 'IGNORE';
+ local $SIG{TSTP} = 'IGNORE';
+ local $SIG{PIPE} = 'IGNORE';
+
+ local $FS::svc_Common::noexport_hack = 1;
+ my $oldAutoCommit = $FS::UID::AutoCommit;
+ local $FS::UID::AutoCommit = 0;
+ my $dbh = dbh;
+
+ my $reset = 0;
+ my %handyhash = ();
+ if ( $options{null} ) {
+ %handyhash = ( map { ( $_ => undef, $_."_threshold" => undef ) }
+ qw( seconds upbytes downbytes totalbytes )
+ );
+ }
+ foreach my $field (keys %$valueref){
+ $reset = 1 if $valueref->{$field};
+ $self->setfield($field, $valueref->{$field});
+ $self->setfield( $field.'_threshold',
+ int($self->getfield($field)
+ * ( $conf->exists('svc_acct-usage_threshold')
+ ? 1 - $conf->config('svc_acct-usage_threshold')/100
+ : 0.20
+ )
+ )
+ );
+ $handyhash{$field} = $self->getfield($field);
+ $handyhash{$field.'_threshold'} = $self->getfield($field.'_threshold');
+ }
+ #my $error = $self->replace; #NO! we avoid the call to ->check for
+ #die $error if $error; #services not explicity changed via the UI
+
+ my $sql = "UPDATE svc_acct SET " .
+ join (',', map { "$_ = ?" } (keys %handyhash) ).
+ " WHERE svcnum = ". $self->svcnum;
+
+ warn "$me $sql\n"
+ if $DEBUG;
+
+ if (scalar(keys %handyhash)) {
+ my $sth = $dbh->prepare( $sql )
+ or die "Error preparing $sql: ". $dbh->errstr;
+ my $rv = $sth->execute(values %handyhash);
+ die "Error executing $sql: ". $sth->errstr
+ unless defined($rv);
+ die "Can't update usage for svcnum ". $self->svcnum
+ if $rv == 0;
+ }
+
+ if ( $conf->exists('radius-chillispot-max') ) {
+ #$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 });
+ local($FS::Record::nowarn_identical) = 1;
+ my $error = $new->replace($self); #call exports
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "Error replacing: $error";
+ }
+ }
+
+ if ( $reset ) {
+
+ 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"
+ if $DEBUG;
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+ '';
+
+}
+
+
+=item recharge HASHREF
+
+ Increments usage columns by the amount specified in HASHREF as
+ column=>amount pairs.
+
+=cut
+
+sub recharge {
+ my ($self, $vhash) = @_;
+
+ if ( $DEBUG ) {
+ warn "[$me] recharge called on $self: ". Dumper($self).
+ "\nwith vhash: ". Dumper($vhash);
+ }
+
+ my $oldAutoCommit = $FS::UID::AutoCommit;
+ local $FS::UID::AutoCommit = 0;
+ my $dbh = dbh;
+ my $error = '';
+
+ foreach my $column (keys %$vhash){
+ $error ||= $self->_op_usage('+', $column, $vhash->{$column});
+ }
+
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ }else{
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+ }
+ return $error;
+}
+
+=item is_rechargeable
+
+Returns true if this svc_account can be "recharged" and false otherwise.
+
+=cut
+
+sub is_rechargable {
+ my $self = shift;
+ $self->seconds ne ''
+ || $self->upbytes ne ''
+ || $self->downbytes ne ''
+ || $self->totalbytes ne '';
+}
+
+=item seconds_since TIMESTAMP
+
+Returns the number of seconds this account has been online since TIMESTAMP,
+according to the session monitor (see L<FS::session>).
+
+TIMESTAMP is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
+L<Time::Local> and L<Date::Parse> for conversion functions.
+
+=cut
+
+#note: POD here, implementation in FS::cust_svc
+sub seconds_since {
+ my $self = shift;
+ $self->cust_svc->seconds_since(@_);
+}
+
+=item last_login_text
+
+Returns text describing the time of last login.
+
+=cut
+
+sub last_login_text {
+ my $self = shift;
+ $self->last_login ? ctime($self->last_login) : 'unknown';
+}
+
+=item psearch_cdrs OPTIONS
+
+Returns a paged search (L<FS::PagedSearch>) for Call Detail Records
+associated with this service. For svc_acct, "associated with" means that
+either the "src" or the "charged_party" field of the CDR matches either
+the "username" field of the service or the username@domain label.
+
+=cut
+
+sub psearch_cdrs {
+ my($self, %options) = @_;
+ my @fields;
+ my %hash;
+ my @where;
+
+ my $did = dbh->quote($self->username);
+ my $diddomain = dbh->quote($self->label);
+
+ my $prefix = $options{'default_prefix'} || ''; #convergent.au '+61'
+ my $prefixdid = dbh->quote($prefix . $self->username);
+
+ my $for_update = $options{'for_update'} ? 'FOR UPDATE' : '';
+
+ if ( $options{inbound} ) {
+ # these will be selected under their DIDs
+ push @where, "FALSE";
+ }
+
+ my @orwhere;
+ if (!$options{'disable_charged_party'}) {
+ push @orwhere,
+ "charged_party = $did",
+ "charged_party = $prefixdid",
+ "charged_party = $diddomain"
+ ;
+ }
+ if (!$options{'disable_src'}) {
+ push @orwhere,
+ "src = $did AND charged_party IS NULL",
+ "src = $prefixdid AND charged_party IS NULL",
+ "src = $diddomain AND charged_party IS NULL"
+ ;
+ }
+ push @where, '(' . join(' OR ', @orwhere) . ')';
+
+ # $options{'status'} = '' is meaningful; for the rest of them it's not
+ if ( exists $options{'status'} ) {
+ $hash{'freesidestatus'} = $options{'status'};
+ }
+ if ( $options{'cdrtypenum'} ) {
+ $hash{'cdrtypenum'} = $options{'cdrtypenum'};
+ }
+ if ( $options{'calltypenum'} ) {
+ $hash{'calltypenum'} = $options{'calltypenum'};
+ }
+ if ( $options{'begin'} ) {
+ push @where, 'startdate >= '. $options{'begin'};
+ }
+ if ( $options{'end'} ) {
+ push @where, 'startdate < '. $options{'end'};
+ }
+ if ( $options{'nonzero'} ) {
+ push @where, 'duration > 0';
+ }
+
+ my $extra_sql = join(' AND ', @where);
+ if ($extra_sql) {
+ if (keys %hash) {
+ $extra_sql = " AND ".$extra_sql;
+ } else {
+ $extra_sql = " WHERE ".$extra_sql;
+ }
+ }
+ return psearch({
+ 'select' => '*',
+ 'table' => 'cdr',
+ 'hashref' => \%hash,
+ 'extra_sql' => $extra_sql,
+ 'order_by' => "ORDER BY startdate $for_update",
+ });
+}
+
+=item get_cdrs (DEPRECATED)
+
+Like psearch_cdrs, but returns all the L<FS::cdr> objects at once, in a
+single list. Arguments are the same as for psearch_cdrs.
+
+=cut
+
+sub get_cdrs {
+ my $self = shift;
+ my $psearch = $self->psearch_cdrs(@_);
+ qsearch ( $psearch->{query} )
+}
+
+# sub radius_groups has moved to svc_Radius_Mixin
+
+=item clone_suspended
+
+Constructor used by FS::part_export::_export_suspend fallback. Document
+better.
+
+=cut
+
+sub clone_suspended {
+ my $self = shift;
+ my %hash = $self->hash;
+ $hash{_password} = join('',map($pw_set[ int(rand $#pw_set) ], (0..7) ) );
+ new FS::svc_acct \%hash;
+}
+
+=item clone_kludge_unsuspend
+
+Constructor used by FS::part_export::_export_unsuspend fallback. Document
+better.
+
+=cut
+
+sub clone_kludge_unsuspend {
+ my $self = shift;
+ my %hash = $self->hash;
+ $hash{_password} = '';
+ new FS::svc_acct \%hash;
+}
+
+=item check_password
+
+Checks the supplied password against the (possibly encrypted) password in the
+database. Returns true for a successful authentication, false for no match.
+
+Currently supported encryptions are: classic DES crypt() and MD5
+
+=cut
+
+sub check_password {
+ my($self, $check_password) = @_;
+
+ #remove old-style SUSPENDED kludge, they should be allowed to login to
+ #self-service and pay up
+ ( my $password = $self->_password ) =~ s/^\*SUSPENDED\* //;
+
+ if ( $self->_password_encoding eq 'ldap' ) {
+
+ $password =~ s/^{PLAIN}/{CLEARTEXT}/;
+ my $auth = from_rfc2307 Authen::Passphrase $password;
+ return $auth->match($check_password);
+
+ } elsif ( $self->_password_encoding eq 'crypt' ) {
+
+ my $auth = from_crypt Authen::Passphrase $self->_password;
+ return $auth->match($check_password);
+
+ } elsif ( $self->_password_encoding eq 'plain' ) {
+
+ return $check_password eq $password;
+
+ } else {
+
+ #XXX this could be replaced with Authen::Passphrase stuff
+
+ if ( $password =~ /^(\*|!!?)$/ ) { #no self-service login
+ return 0;
+ } elsif ( length($password) < 13 ) { #plaintext
+ $check_password eq $password;
+ } elsif ( length($password) == 13 ) { #traditional DES crypt
+ crypt($check_password, $password) eq $password;
+ } elsif ( $password =~ /^\$1\$/ ) { #MD5 crypt
+ unix_md5_crypt($check_password, $password) eq $password;
+ } elsif ( $password =~ /^\$2a?\$/ ) { #Blowfish
+ warn "Can't check password: Blowfish encryption not yet supported, ".
+ "svcnum ". $self->svcnum. "\n";
+ 0;
+ } else {
+ warn "Can't check password: Unrecognized encryption for svcnum ".
+ $self->svcnum. "\n";
+ 0;
+ }
+
+ }
+
+}
+
+=item crypt_password [ DEFAULT_ENCRYPTION_TYPE ]
+
+Returns an encrypted password, either by passing through an encrypted password
+in the database or by encrypting a plaintext password from the database.
+
+The optional DEFAULT_ENCRYPTION_TYPE parameter can be set to I<crypt> (classic
+UNIX DES crypt), I<md5> (md5 crypt supported by most modern Linux and BSD
+distrubtions), or (eventually) I<blowfish> (blowfish hashing supported by
+OpenBSD, SuSE, other Linux distibutions with pam_unix2, etc.). The default
+encryption type is only used if the password is not already encrypted in the
+database.
+
+=cut
+
+sub crypt_password {
+ my $self = shift;
+
+ if ( $self->_password_encoding eq 'ldap' ) {
+
+ if ( $self->_password =~ /^\{(PLAIN|CLEARTEXT)\}(.+)$/ ) {
+ my $plain = $2;
+
+ #XXX this could be replaced with Authen::Passphrase stuff
+
+ my $encryption = ( scalar(@_) && $_[0] ) ? shift : 'crypt';
+ if ( $encryption eq 'crypt' ) {
+ return crypt(
+ $self->_password,
+ $saltset[int(rand(64))].$saltset[int(rand(64))]
+ );
+ } elsif ( $encryption eq 'md5' ) {
+ return unix_md5_crypt( $self->_password );
+ } elsif ( $encryption eq 'blowfish' ) {
+ croak "unknown encryption method $encryption";
+ } else {
+ croak "unknown encryption method $encryption";
+ }
+
+ } elsif ( $self->_password =~ /^\{CRYPT\}(.+)$/ ) {
+ return $1;
+ }
+
+ } elsif ( $self->_password_encoding eq 'crypt' ) {
+
+ return $self->_password;
+
+ } elsif ( $self->_password_encoding eq 'plain' ) {
+
+ #XXX this could be replaced with Authen::Passphrase stuff
+
+ my $encryption = ( scalar(@_) && $_[0] ) ? shift : 'crypt';
+ if ( $encryption eq 'crypt' ) {
+ return crypt(
+ $self->_password,
+ $saltset[int(rand(64))].$saltset[int(rand(64))]
+ );
+ } elsif ( $encryption eq 'md5' ) {
+ return unix_md5_crypt( $self->_password );
+ } elsif ( $encryption eq 'sha512' ) {
+ return crypt(
+ $self->_password,
+ '$6$rounds=15420$'. join('', map $saltset[int(rand(64))], (1..16) )
+ );
+ } elsif ( $encryption eq 'sha1_base64' ) { #for acct_sql
+ my $pass = sha1_base64( $self->_password );
+ $pass .= '=' x (4 - length($pass) % 4); #properly padded base64
+ return $pass;
+ } elsif ( $encryption eq 'blowfish' ) {
+ croak "unknown encryption method $encryption";
+ } else {
+ croak "unknown encryption method $encryption";
+ }
+
+ } else {
+
+ if ( length($self->_password) == 13
+ || $self->_password =~ /^\$(1|2a?)\$/
+ || $self->_password =~ /^(\*|NP|\*LK\*|!!?)$/
+ )
+ {
+ $self->_password;
+ } else {
+
+ #XXX this could be replaced with Authen::Passphrase stuff
+
+ my $encryption = ( scalar(@_) && $_[0] ) ? shift : 'crypt';
+ if ( $encryption eq 'crypt' ) {
+ return crypt(
+ $self->_password,
+ $saltset[int(rand(64))].$saltset[int(rand(64))]
+ );
+ } elsif ( $encryption eq 'md5' ) {
+ return unix_md5_crypt( $self->_password );
+ } elsif ( $encryption eq 'blowfish' ) {
+ croak "unknown encryption method $encryption";
+ } else {
+ croak "unknown encryption method $encryption";
+ }
+
+ }
+
+ }
+
+}
+
+=item ldap_password [ DEFAULT_ENCRYPTION_TYPE ]
+
+Returns an encrypted password in "LDAP" format, with a curly-bracked prefix
+describing the format, for example, "{PLAIN}himom", "{CRYPT}94pAVyK/4oIBk" or
+"{MD5}5426824942db4253f87a1009fd5d2d4".
+
+The optional DEFAULT_ENCRYPTION_TYPE is not yet used, but the idea is for it
+to work the same as the B</crypt_password> method.
+
+=cut
+
+sub ldap_password {
+ my $self = shift;
+ #eventually should check a "password-encoding" field
+
+ if ( $self->_password_encoding eq 'ldap' ) {
+
+ return $self->_password;
+
+ } elsif ( $self->_password_encoding eq 'crypt' ) {
+
+ if ( length($self->_password) == 13 ) { #crypt
+ return '{CRYPT}'. $self->_password;
+ } elsif ( $self->_password =~ /^\$1\$(.*)$/ && length($1) == 31 ) { #passwdMD5
+ return '{MD5}'. $1;
+ #} elsif ( $self->_password =~ /^\$2a?\$(.*)$/ ) { #Blowfish
+ # die "Blowfish encryption not supported in this context, svcnum ".
+ # $self->svcnum. "\n";
+ } else {
+ warn "encryption method not (yet?) supported in LDAP context";
+ return '{CRYPT}*'; #unsupported, should not auth
+ }
+
+ } elsif ( $self->_password_encoding eq 'plain' ) {
+
+ return '{PLAIN}'. $self->_password;
+
+ #return '{CLEARTEXT}'. $self->_password; #?
+
+ } else {
+
+ if ( length($self->_password) == 13 ) { #crypt
+ return '{CRYPT}'. $self->_password;
+ } elsif ( $self->_password =~ /^\$1\$(.*)$/ && length($1) == 31 ) { #passwdMD5
+ return '{MD5}'. $1;
+ } elsif ( $self->_password =~ /^\$2a?\$(.*)$/ ) { #Blowfish
+ warn "Blowfish encryption not supported in this context, svcnum ".
+ $self->svcnum. "\n";
+ return '{CRYPT}*';
+
+ #are these two necessary anymore?
+ } elsif ( $self->_password =~ /^(\w{48})$/ ) { #LDAP SSHA
+ return '{SSHA}'. $1;
+ } elsif ( $self->_password =~ /^(\w{64})$/ ) { #LDAP NS-MTA-MD5
+ return '{NS-MTA-MD5}'. $1;
+
+ } else { #plaintext
+ return '{PLAIN}'. $self->_password;
+
+ #return '{CLEARTEXT}'. $self->_password; #?
+
+ #XXX this could be replaced with Authen::Passphrase stuff if it gets used
+ #my $encryption = ( scalar(@_) && $_[0] ) ? shift : 'crypt';
+ #if ( $encryption eq 'crypt' ) {
+ # return '{CRYPT}'. crypt(
+ # $self->_password,
+ # $saltset[int(rand(64))].$saltset[int(rand(64))]
+ # );
+ #} elsif ( $encryption eq 'md5' ) {
+ # unix_md5_crypt( $self->_password );
+ #} elsif ( $encryption eq 'blowfish' ) {
+ # croak "unknown encryption method $encryption";
+ #} else {
+ # croak "unknown encryption method $encryption";
+ #}
+ }
+
+ }
+
+}
+
+=item domain_slash_username
+
+Returns $domain/$username/
+
+=cut
+
+sub domain_slash_username {
+ my $self = shift;
+ $self->domain. '/'. $self->username. '/';
+}
+
+=item virtual_maildir
+
+Returns $domain/maildirs/$username/
+
+=cut
+
+sub virtual_maildir {
+ my $self = shift;
+ $self->domain. '/maildirs/'. $self->username. '/';
+}
+
+=item password_svc_check
+
+Override, for L<FS::Password_Mixin>. Not really intended for other use.
+
+=cut
+
+sub password_svc_check {
+ my ($self, $password) = @_;
+ foreach my $field ( qw(username finger) ) {
+ foreach my $word (split(/\W+/,$self->get($field))) {
+ next unless length($word) > 2;
+ if ($password =~ /$word/i) {
+ return qq(Password contains account information '$word');
+ }
+ }
+ }
+ return '';
+}
+
+=back
+
+=head1 CLASS METHODS
+
+=over 4
+
+=item search HASHREF
+
+Class method which returns a qsearch hash expression to search for parameters
+specified in HASHREF. Valid parameters are
+
+=over 4
+
+=item domain
+
+=item domsvc
+
+=item unlinked
+
+=item agentnum
+
+=item pkgpart
+
+Arrayref of pkgparts
+
+=item pkgpart
+
+=item where
+
+Arrayref of additional WHERE clauses, will be ANDed together.
+
+=item order_by
+
+=item cust_fields
+
+=back
+
+=cut
+
+sub _search_svc {
+ my( $class, $params, $from, $where ) = @_;
+
+ #these two should probably move to svc_Domain_Mixin ?
+
+ # domain
+ if ( $params->{'domain'} ) {
+ my $svc_domain = qsearchs('svc_domain', { 'domain'=>$params->{'domain'} } );
+ #preserve previous behavior & bubble up an error if $svc_domain not found?
+ push @$where, 'domsvc = '. $svc_domain->svcnum if $svc_domain;
+ }
+
+ # domsvc
+ if ( $params->{'domsvc'} =~ /^(\d+)$/ ) {
+ push @$where, "domsvc = $1";
+ }
+
+
+ # popnum
+ if ( $params->{'popnum'} =~ /^(\d+)$/ ) {
+ push @$where, "popnum = $1";
+ }
+
+
+ #and these in svc_Tower_Mixin, or maybe we never should have done svc_acct
+ # towers (or, as mark thought, never should have done svc_broadband)
+
+ # sector and tower
+ my @where_sector = $class->tower_sector_sql($params);
+ if ( @where_sector ) {
+ push @$where, @where_sector;
+ push @$from, ' LEFT JOIN tower_sector USING ( sectornum )';
+ }
+
+}
+
+=back
+
+=head1 SUBROUTINES
+
+=over 4
+
+=item check_and_rebuild_fuzzyfiles
+
+=cut
+
+sub check_and_rebuild_fuzzyfiles {
+ my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
+ -e "$dir/svc_acct.username"
+ or &rebuild_fuzzyfiles;
+}
+
+=item rebuild_fuzzyfiles
+
+=cut
+
+sub rebuild_fuzzyfiles {
+
+ use Fcntl qw(:flock);
+
+ my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
+
+ #username
+
+ open(USERNAMELOCK,">>$dir/svc_acct.username")
+ or die "can't open $dir/svc_acct.username: $!";
+ flock(USERNAMELOCK,LOCK_EX)
+ or die "can't lock $dir/svc_acct.username: $!";
+
+ my @all_username = map $_->getfield('username'), qsearch('svc_acct', {});
+
+ open (USERNAMECACHE,">$dir/svc_acct.username.tmp")
+ or die "can't open $dir/svc_acct.username.tmp: $!";
+ print USERNAMECACHE join("\n", @all_username), "\n";
+ close USERNAMECACHE or die "can't close $dir/svc_acct.username.tmp: $!";
+
+ rename "$dir/svc_acct.username.tmp", "$dir/svc_acct.username";
+ close USERNAMELOCK;
+
+}
+
+=item all_username
+
+=cut
+
+sub all_username {
+ my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
+ open(USERNAMECACHE,"<$dir/svc_acct.username")
+ or die "can't open $dir/svc_acct.username: $!";
+ my @array = map { chomp; $_; } <USERNAMECACHE>;
+ close USERNAMECACHE;
+ \@array;
+}
+
+=item append_fuzzyfiles USERNAME
+
+=cut
+
+sub append_fuzzyfiles {
+ my $username = shift;
+
+ &check_and_rebuild_fuzzyfiles;
+
+ use Fcntl qw(:flock);
+
+ my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
+
+ open(USERNAME,">>$dir/svc_acct.username")
+ or die "can't open $dir/svc_acct.username: $!";
+ flock(USERNAME,LOCK_EX)
+ or die "can't lock $dir/svc_acct.username: $!";
+
+ print USERNAME "$username\n";
+
+ flock(USERNAME,LOCK_UN)
+ or die "can't unlock $dir/svc_acct.username: $!";
+ close USERNAME;
+
+ 1;
+}
+
+
+=item reached_threshold
+
+Performs some activities when svc_acct thresholds (such as number of seconds
+remaining) are reached.
+
+=cut
+
+sub reached_threshold {
+ my %opt = @_;
+
+ my $svc_acct = qsearchs('svc_acct', { 'svcnum' => $opt{'svcnum'} } );
+ die "Cannot find svc_acct with svcnum " . $opt{'svcnum'} unless $svc_acct;
+
+ if ( $opt{'op'} eq '+' ){
+ $svc_acct->setfield( $opt{'column'}.'_threshold',
+ int($svc_acct->getfield($opt{'column'})
+ * ( $conf->exists('svc_acct-usage_threshold')
+ ? $conf->config('svc_acct-usage_threshold')/100
+ : 0.80
+ )
+ )
+ );
+ my $error = $svc_acct->replace;
+ die $error if $error;
+ }elsif ( $opt{'op'} eq '-' ){
+
+ my $threshold = $svc_acct->getfield( $opt{'column'}.'_threshold' );
+ return '' if ($threshold eq '' );
+
+ $svc_acct->setfield( $opt{'column'}.'_threshold', 0 );
+ my $error = $svc_acct->replace;
+ die $error if $error; # email next time, i guess
+
+ if ( $warning_msgnum ) {
+
+ my $msg_template = qsearchs('msg_template',{ msgnum => $warning_msgnum });
+ die "Could not load template for threshold_warning_msgnum ($warning_msgnum)" unless $msg_template;
+
+ my $cust_main = $svc_acct->cust_svc->cust_pkg->cust_main;
+
+ my $to = join(', ', $cust_main->invoicing_list_emailonly );
+
+ my $error = $msg_template->send(
+ cust_main => $cust_main,
+ object => $svc_acct,
+ to => $to,
+ substitutions => {
+ # have to override these, because we changed threshold above
+ 'column' => $opt{'column'},
+ 'amount' => $opt{'column'} =~/bytes/
+ ? FS::UI::bytecount::display_bytecount($svc_acct->getfield($opt{'column'}))
+ : $svc_acct->getfield($opt{'column'}),
+ 'threshold' => $opt{'column'} =~/bytes/
+ ? FS::UI::bytecount::display_bytecount($threshold)
+ : $threshold,
+ },
+ );
+
+ die "Error sending threshold warning email: $error" if $error;
+
+ }
+ }else{
+ die "unknown op: " . $opt{'op'};
+ }
+}
+
+=back
+
+=head1 BUGS
+
+The $recref stuff in sub check should be cleaned up.
+
+The suspend, unsuspend and cancel methods update the database, but not the
+current object. This is probably a bug as it's unexpected and
+counterintuitive.
+
+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,
+export.html from the base documentation, L<FS::Record>, L<FS::Conf>,
+L<FS::cust_svc>, L<FS::part_svc>, L<FS::cust_pkg>, L<FS::queue>,
+L<freeside-queued>), L<FS::svc_acct_pop>,
+schema.html from the base documentation.
+
+=cut
+
+1;