package FS::cust_svc;
use strict;
-use vars qw( @ISA );
+use vars qw( @ISA $ignore_quantity );
use Carp qw( cluck );
-use FS::Record qw( qsearchs dbh );
+use FS::Record qw( qsearch qsearchs dbh );
use FS::cust_pkg;
use FS::part_pkg;
use FS::part_svc;
+use FS::pkg_svc;
use FS::svc_acct;
-use FS::svc_acct_sm;
use FS::svc_domain;
use FS::svc_forward;
+use FS::svc_broadband;
use FS::domain_record;
+use FS::part_export;
@ISA = qw( FS::Record );
+$ignore_quantity = 0;
+
sub _cache {
my $self = shift;
my ( $hashref, $cache ) = @_;
=item delete
Deletes this service from the database. If there is an error, returns the
-error, otherwise returns false.
+error, otherwise returns false. Note that this only removes the cust_svc
+record - you should probably use the B<cancel> method instead.
+
+=item cancel
+
+Cancels the relevant service by calling the B<cancel> method of the associated
+FS::svc_XXX object (i.e. an FS::svc_acct object or FS::svc_domain object),
+deleting the FS::svc_XXX record and then deleting this record.
+
+If there is an error, returns the error, otherwise returns false.
+
+=cut
-Called by the cancel method of the package (see L<FS::cust_pkg>).
+sub cancel {
+ my $self = shift;
+
+ 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 $part_svc = $self->part_svc;
+
+ $part_svc->svcdb =~ /^([\w\-]+)$/ or do {
+ $dbh->rollback if $oldAutoCommit;
+ return "Illegal svcdb value in part_svc!";
+ };
+ my $svcdb = $1;
+ require "FS/$svcdb.pm";
+
+ my $svc = $self->svc_x;
+ if ($svc) {
+ my $error = $svc->cancel;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "Error canceling service: $error";
+ }
+ $error = $svc->delete;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "Error deleting service: $error";
+ }
+ }
+
+ my $error = $self->delete;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "Error deleting cust_svc: $error";
+ }
+
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+ ''; #no errors
+
+}
=item replace OLD_RECORD
Replaces the OLD_RECORD with this one in the database. If there is an error,
returns the error, otherwise returns false.
+=cut
+
+sub replace {
+ my ( $new, $old ) = ( shift, shift );
+
+ 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 $error = $new->SUPER::replace($old);
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error if $error;
+ }
+
+ if ( $new->svcpart != $old->svcpart ) {
+ my $svc_x = $new->svc_x;
+ my $new_svc_x = ref($svc_x)->new({$svc_x->hash});
+ my $error = $new_svc_x->replace($svc_x);
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error if $error;
+ }
+ }
+
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+ ''; #no error
+
+}
+
=item check
Checks all fields to make sure this is a valid service. If there is an error,
;
return $error if $error;
- return "Unknown pkgnum"
- unless ! $self->pkgnum
- || qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
-
- return "Unknown svcpart" unless
- qsearchs( 'part_svc', { 'svcpart' => $self->svcpart } );
+ my $part_svc = qsearchs( 'part_svc', { 'svcpart' => $self->svcpart } );
+ return "Unknown svcpart" unless $part_svc;
+
+ if ( $self->pkgnum ) {
+ my $cust_pkg = qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
+ return "Unknown pkgnum" unless $cust_pkg;
+ my $pkg_svc = qsearchs( 'pkg_svc', {
+ 'pkgpart' => $cust_pkg->pkgpart,
+ 'svcpart' => $self->svcpart,
+ });
+ # or new FS::pkg_svc ( { 'pkgpart' => $cust_pkg->pkgpart,
+ # 'svcpart' => $self->svcpart,
+ # 'quantity' => 0 } );
+ my $quantity = $pkg_svc ? $pkg_svc->quantity : 0;
+
+ my @cust_svc = qsearch('cust_svc', {
+ 'pkgnum' => $self->pkgnum,
+ 'svcpart' => $self->svcpart,
+ });
+ return "Already ". scalar(@cust_svc). " ". $part_svc->svc.
+ " services for pkgnum ". $self->pkgnum
+ if scalar(@cust_svc) >= $quantity && !$ignore_quantity;
+ }
- ''; #no error
+ $self->SUPER::check;
}
=item part_svc
my $tag;
if ( $svcdb eq 'svc_acct' ) {
$tag = $svc_x->email;
- } elsif ( $svcdb eq 'svc_acct_sm' ) {
- my $domuser = $svc_x->domuser eq '*' ? '(anything)' : $svc_x->domuser;
- my $svc_domain = qsearchs ( 'svc_domain', { 'svcnum' => $svc_x->domsvc } );
- my $domain = $svc_domain->domain;
- $tag = "$domuser\@$domain";
} elsif ( $svcdb eq 'svc_forward' ) {
my $svc_acct = qsearchs( 'svc_acct', { 'svcnum' => $svc_x->srcsvc } );
$tag = $svc_acct->email. '->';
$tag = $svc_x->getfield('domain');
} elsif ( $svcdb eq 'svc_www' ) {
my $domain = qsearchs( 'domain_record', { 'recnum' => $svc_x->recnum } );
- $tag = $domain->reczone;
+ $tag = $domain->zone;
+ } elsif ( $svcdb eq 'svc_broadband' ) {
+ $tag = $svc_x->ip_addr;
} else {
cluck "warning: asked for label of unsupported svcdb; using svcnum";
$tag = $svc_x->getfield('svcnum');
$sth->fetchrow_arrayref->[0];
}
-=back
+=item seconds_since_sqlradacct TIMESTAMP_START TIMESTAMP_END
+
+See L<FS::svc_acct/seconds_since_sqlradacct>. Equivalent to
+$cust_svc->svc_x->seconds_since_sqlradacct, but more efficient. Meaningless
+for records where B<svcdb> is not "svc_acct".
+
+=cut
+
+#note: implementation here, POD in FS::svc_acct
+sub seconds_since_sqlradacct {
+ my($self, $start, $end) = @_;
+
+ my $username = $self->svc_x->username;
+
+ my @part_export = $self->part_svc->part_export('sqlradius')
+ or die "no sqlradius export configured for this service type";
+ #or return undef;
-=head1 VERSION
+ my $seconds = 0;
+ foreach my $part_export ( @part_export ) {
-$Id: cust_svc.pm,v 1.9 2002-01-29 16:33:15 ivan Exp $
+ my $dbh = DBI->connect( map { $part_export->option($_) }
+ qw(datasrc username password) )
+ or die "can't connect to sqlradius database: ". $DBI::errstr;
+
+ #select a unix time conversion function based on database type
+ my $str2time;
+ if ( $dbh->{Driver}->{Name} eq 'mysql' ) {
+ $str2time = 'UNIX_TIMESTAMP(';
+ } elsif ( $dbh->{Driver}->{Name} eq 'Pg' ) {
+ $str2time = 'EXTRACT( EPOCH FROM ';
+ } else {
+ warn "warning: unknown database type ". $dbh->{Driver}->{Name}.
+ "; guessing how to convert to UNIX timestamps";
+ $str2time = 'extract(epoch from ';
+ }
+
+ my $query;
+
+ #find closed sessions completely within the given range
+ my $sth = $dbh->prepare("SELECT SUM(acctsessiontime)
+ FROM radacct
+ WHERE UserName = ?
+ AND $str2time AcctStartTime) >= ?
+ AND $str2time AcctStopTime ) < ?
+ AND $str2time AcctStopTime ) > 0
+ AND AcctStopTime IS NOT NULL"
+ ) or die $dbh->errstr;
+ $sth->execute($username, $start, $end) or die $sth->errstr;
+ my $regular = $sth->fetchrow_arrayref->[0];
+
+ #find open sessions which start in the range, count session start->range end
+ $query = "SELECT SUM( ? - $str2time AcctStartTime ) )
+ FROM radacct
+ WHERE UserName = ?
+ AND $str2time AcctStartTime ) >= ?
+ AND $str2time AcctStartTime ) < ?
+ AND ( ? - $str2time AcctStartTime ) ) < 86400
+ AND ( $str2time AcctStopTime ) = 0
+ OR AcctStopTime IS NULL )";
+ $sth = $dbh->prepare($query) or die $dbh->errstr;
+ $sth->execute($end, $username, $start, $end, $end)
+ or die $sth->errstr. " executing query $query";
+ my $start_during = $sth->fetchrow_arrayref->[0];
+
+ #find closed sessions which start before the range but stop during,
+ #count range start->session end
+ $sth = $dbh->prepare("SELECT SUM( $str2time AcctStopTime ) - ? )
+ FROM radacct
+ WHERE UserName = ?
+ AND $str2time AcctStartTime ) < ?
+ AND $str2time AcctStopTime ) >= ?
+ AND $str2time AcctStopTime ) < ?
+ AND $str2time AcctStopTime ) > 0
+ AND AcctStopTime IS NOT NULL"
+ ) or die $dbh->errstr;
+ $sth->execute($start, $username, $start, $start, $end ) or die $sth->errstr;
+ my $end_during = $sth->fetchrow_arrayref->[0];
+
+ #find closed (not anymore - or open) sessions which start before the range
+ # but stop after, or are still open, count range start->range end
+ # don't count open sessions (probably missing stop record)
+ $sth = $dbh->prepare("SELECT COUNT(*)
+ FROM radacct
+ WHERE UserName = ?
+ AND $str2time AcctStartTime ) < ?
+ AND ( $str2time AcctStopTime ) >= ?
+ )"
+ # OR AcctStopTime = 0
+ # OR AcctStopTime IS NULL )"
+ ) or die $dbh->errstr;
+ $sth->execute($username, $start, $end ) or die $sth->errstr;
+ my $entire_range = ($end-$start) * $sth->fetchrow_arrayref->[0];
+
+ $seconds += $regular + $end_during + $start_during + $entire_range;
+
+ }
+
+ $seconds;
+
+}
+
+=item attribute_since_sqlradacct TIMESTAMP_START TIMESTAMP_END ATTRIBUTE
+
+See L<FS::svc_acct/attribute_since_sqlradacct>. Equivalent to
+$cust_svc->svc_x->attribute_since_sqlradacct, but more efficient. Meaningless
+for records where B<svcdb> is not "svc_acct".
+
+=cut
+
+#note: implementation here, POD in FS::svc_acct
+#(false laziness w/seconds_since_sqlradacct above)
+sub attribute_since_sqlradacct {
+ my($self, $start, $end, $attrib) = @_;
+
+ my $username = $self->svc_x->username;
+
+ my @part_export = $self->part_svc->part_export('sqlradius')
+ or die "no sqlradius export configured for this service type";
+ #or return undef;
+
+ my $sum = 0;
+
+ foreach my $part_export ( @part_export ) {
+
+ my $dbh = DBI->connect( map { $part_export->option($_) }
+ qw(datasrc username password) )
+ or die "can't connect to sqlradius database: ". $DBI::errstr;
+
+ #select a unix time conversion function based on database type
+ my $str2time;
+ if ( $dbh->{Driver}->{Name} eq 'mysql' ) {
+ $str2time = 'UNIX_TIMESTAMP(';
+ } elsif ( $dbh->{Driver}->{Name} eq 'Pg' ) {
+ $str2time = 'EXTRACT( EPOCH FROM ';
+ } else {
+ warn "warning: unknown database type ". $dbh->{Driver}->{Name}.
+ "; guessing how to convert to UNIX timestamps";
+ $str2time = 'extract(epoch from ';
+ }
+
+ my $sth = $dbh->prepare("SELECT SUM($attrib)
+ FROM radacct
+ WHERE UserName = ?
+ AND $str2time AcctStopTime ) >= ?
+ AND $str2time AcctStopTime ) < ?
+ AND AcctStopTime IS NOT NULL"
+ ) or die $dbh->errstr;
+ $sth->execute($username, $start, $end) or die $sth->errstr;
+
+ $sum += $sth->fetchrow_arrayref->[0];
+
+ }
+
+ $sum;
+
+}
+
+=item get_session_history_sqlradacct TIMESTAMP_START TIMESTAMP_END
+
+See L<FS::svc_acct/get_session_history_sqlradacct>. Equivalent to
+$cust_svc->svc_x->get_session_history_sqlradacct, but more efficient.
+Meaningless for records where B<svcdb> is not "svc_acct".
+
+=cut
+
+sub get_session_history {
+ my($self, $start, $end, $attrib) = @_;
+
+ my $username = $self->svc_x->username;
+
+ my @part_export = $self->part_svc->part_export('sqlradius')
+ or die "no sqlradius export configured for this service type";
+ #or return undef;
+
+ my @sessions = ();
+
+ foreach my $part_export ( @part_export ) {
+
+ my $dbh = DBI->connect( map { $part_export->option($_) }
+ qw(datasrc username password) )
+ or die "can't connect to sqlradius database: ". $DBI::errstr;
+
+ #select a unix time conversion function based on database type
+ my $str2time;
+ if ( $dbh->{Driver}->{Name} eq 'mysql' ) {
+ $str2time = 'UNIX_TIMESTAMP(';
+ } elsif ( $dbh->{Driver}->{Name} eq 'Pg' ) {
+ $str2time = 'EXTRACT( EPOCH FROM ';
+ } else {
+ warn "warning: unknown database type ". $dbh->{Driver}->{Name}.
+ "; guessing how to convert to UNIX timestamps";
+ $str2time = 'extract(epoch from ';
+ }
+
+ my @fields = qw( acctstarttime acctstoptime acctsessiontime
+ acctinputoctets acctoutputoctets framedipaddress );
+
+ my $sth = $dbh->prepare('SELECT '. join(', ', @fields).
+ " FROM radacct
+ WHERE UserName = ?
+ AND $str2time AcctStopTime ) >= ?
+ AND $str2time AcctStopTime ) <= ?
+ ORDER BY AcctStartTime DESC
+ ") or die $dbh->errstr;
+ $sth->execute($username, $start, $end) or die $sth->errstr;
+
+ push @sessions, map { { %$_ } } @{ $sth->fetchall_arrayref({}) };
+
+ }
+ \@sessions
+
+}
+
+=back
=head1 BUGS
Deleting this record doesn't check or delete the svc_* record associated
with this record.
+In seconds_since_sqlradacct, specifying a DATASRC/USERNAME/PASSWORD instead of
+a DBI database handle is not yet implemented.
+
=head1 SEE ALSO
L<FS::Record>, L<FS::cust_pkg>, L<FS::part_svc>, L<FS::pkg_svc>,