X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=blobdiff_plain;f=FS%2FFS%2Fcust_svc.pm;h=d91fa0d7ae43c4b23282bba051f801ad32d47243;hp=c4a75f77a99c79ff226a7f0b218ddf5f7005cbbb;hb=791e374dc2f6d6cecd51e2fad52d74587bf879bf;hpb=9006e983ebf98b2000a812ab01f99dcb2335534c diff --git a/FS/FS/cust_svc.pm b/FS/FS/cust_svc.pm index c4a75f77a..d91fa0d7a 100644 --- a/FS/FS/cust_svc.pm +++ b/FS/FS/cust_svc.pm @@ -1,29 +1,46 @@ package FS::cust_svc; +use base qw( FS::cust_main_Mixin FS::option_Common ); #FS::Record ); use strict; -use vars qw( @ISA $DEBUG $me $ignore_quantity ); -use Carp; +use vars qw( $DEBUG $me $ignore_quantity $conf $ticket_system ); +use Carp qw(cluck); #use Scalar::Util qw( blessed ); +use List::Util qw( max ); use FS::Conf; -use FS::Record qw( qsearch qsearchs dbh str2time_sql ); -use FS::cust_pkg; +use FS::Record qw( qsearch qsearchs dbh str2time_sql str2time_sql_closing ); use FS::part_pkg; use FS::part_svc; use FS::pkg_svc; +use FS::part_svc_link; use FS::domain_record; use FS::part_export; use FS::cdr; +use FS::UI::Web; #most FS::svc_ classes are autoloaded in svc_x emthod use FS::svc_acct; #this one is used in the cache stuff -@ISA = qw( FS::cust_main_Mixin FS::option_Common ); #FS::Record ); $DEBUG = 0; $me = '[cust_svc]'; $ignore_quantity = 0; +#ask FS::UID to run this stuff for us later +FS::UID->install_callback( sub { + $conf = new FS::Conf; + $ticket_system = $conf->config('ticket_system') +}); + +our $cache_enabled = 0; + +sub _simplecache { + my( $self, $hashref ) = @_; + if ( $cache_enabled && $hashref->{'svc'} ) { + $self->{'_svcpart'} = FS::part_svc->new($hashref); + } +} + sub _cache { my $self = shift; my ( $hashref, $cache ) = @_; @@ -69,6 +86,8 @@ The following fields are currently supported: =item svcpart - Service definition (see L) +=item agent_svcid - Optional legacy service ID + =item overlimit - date the service exceeded its usage limit =back @@ -92,12 +111,149 @@ sub table { 'cust_svc'; } Adds this service to the database. If there is an error, returns the error, otherwise returns false. +=cut + +sub insert { + 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 $error = $self->SUPER::insert; + + #check if this releases a hold (see FS::pkg_svc provision_hold) + $error ||= $self->_check_provision_hold; + + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error if $error + } + + $dbh->commit or die $dbh->errstr if $oldAutoCommit; + ''; #no error + +} + =item delete Deletes this service from the database. If there is an error, returns the error, otherwise returns false. Note that this only removes the cust_svc record - you should probably use the B method instead. +=cut + +my $rt_session; + +sub delete { + my $self = shift; + + my $cust_pkg = $self->cust_pkg; + my $custnum = $cust_pkg->custnum if $cust_pkg; + + 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 = $self->SUPER::delete; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + + foreach my $part_svc_link ( $self->part_svc_link( + link_type => 'cust_svc_unprovision_cascade', + ) + ) { + foreach my $cust_svc ( qsearch( 'cust_svc', { + 'pkgnum' => $self->pkgnum, + 'svcpart' => $part_svc_link->dst_svcpart, + }) + ) { + my $error = $cust_svc->svc_x->delete; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + } + + } + + $dbh->commit or die $dbh->errstr if $oldAutoCommit; + + if ( $ticket_system eq 'RT_Internal' ) { + unless ( $rt_session ) { + FS::TicketSystem->init; + $rt_session = FS::TicketSystem->session; + } + my $links = RT::Links->new($rt_session->{CurrentUser}); + my $svcnum = $self->svcnum; + $links->Limit(FIELD => 'Target', + VALUE => 'freeside://freeside/cust_svc/'.$svcnum); + while ( my $l = $links->Next ) { + my ($val, $msg); + if ( $custnum ) { + # re-link to point to the customer instead + ($val, $msg) = + $l->SetTarget('freeside://freeside/cust_main/'.$custnum); + } else { + # unlinked service + ($val, $msg) = $l->Delete; + } + # can't do anything useful on error + warn "error unlinking ticket $svcnum: $msg\n" if !$val; + } + } + + ''; + +} + +=item suspend + +Suspends the relevant service by calling the B method of the associated +FS::svc_XXX object (i.e. an FS::svc_acct object or FS::svc_domain object). + +If there is an error, returns the error, otherwise returns false. + +=cut + +sub suspend { + my( $self, %opt ) = @_; + + $self->part_svc->svcdb =~ /^([\w\-]+)$/ or return 'Illegal part_svc.svcdb'; + my $svcdb = $1; + require "FS/$svcdb.pm"; + + my $svc = qsearchs( $svcdb, { 'svcnum' => $self->svcnum } ) + or return ''; + + my $error = $svc->suspend; + return $error if $error; + + if ( $opt{labels_arryref} ) { + my( $label, $value ) = $self->label; + push @{ $opt{labels_arrayref} }, "$label: $value"; + } + + ''; + +} + =item cancel Cancels the relevant service by calling the B method of the associated @@ -109,7 +265,7 @@ If there is an error, returns the error, otherwise returns false. =cut sub cancel { - my $self = shift; + my($self,%opt) = @_; local $SIG{HUP} = 'IGNORE'; local $SIG{INT} = 'IGNORE'; @@ -133,19 +289,26 @@ sub cancel { 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; #this deletes this cust_svc record as well - if ( $error ) { - $dbh->rollback if $oldAutoCommit; - return "Error deleting service: $error"; + if ( %opt && $opt{'date'} ) { + my $error = $svc->expire($opt{'date'}); + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "Error expiring service: $error"; + } + } else { + my $error = $svc->cancel; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "Error canceling service: $error"; + } + $error = $svc->delete; #this deletes this cust_svc record as well + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "Error deleting service: $error"; + } } - } else { + } elsif ( !%opt ) { #huh? warn "WARNING: no svc_ record found for svcnum ". $self->svcnum. @@ -251,11 +414,66 @@ sub replace { } } +# #trigger a re-export on pkgnum changes? +# # (of prepaid packages), for Expiration RADIUS attribute +# if ( $new->pkgnum != $old->pkgnum && $new->cust_pkg->part_pkg->is_prepaid ) { +# my $svc_x = $new->svc_x; +# local($FS::Record::nowarn_identical) = 1; +# my $error = $svc_x->export('replace'); +# if ( $error ) { +# $dbh->rollback if $oldAutoCommit; +# return $error if $error; +# } +# } + + #trigger a pkg_change export on pkgnum changes + if ( $new->pkgnum != $old->pkgnum ) { + my $error = $new->svc_x->export('pkg_change', $new->cust_pkg, + $old->cust_pkg, + ); + + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error if $error; + } + } # if pkgnum is changing + #my $error = $new->SUPER::replace($old, @_); my $error = $new->SUPER::replace($old); + + #trigger a relocate export on location changes + if ( $new->cust_pkg->locationnum != $old->cust_pkg->locationnum ) { + my $svc_x = $new->svc_x; + if ( $svc_x->locationnum ) { + if ( $svc_x->locationnum == $old->cust_pkg->locationnum ) { + # in this case, set the service location to be the same as the new + # package location + $svc_x->set('locationnum', $new->cust_pkg->locationnum); + # and replace it, which triggers a relocate export so we don't + # need to + $error ||= $svc_x->replace; + } else { + # the service already has a different location from its package + # so don't change it + } + } else { + # the service doesn't have a locationnum (either isn't of a type + # that has the locationnum field, or the locationnum is null and + # defaults to cust_pkg->locationnum) + # so just trigger the export here + $error ||= $new->svc_x->export('relocate', + $new->cust_pkg->cust_location, + $old->cust_pkg->cust_location, + ); + } # if ($svc_x->locationnum) + } # if this is a location change + + #check if this releases a hold (see FS::pkg_svc provision_hold) + $error ||= $new->_check_provision_hold; + if ( $error ) { $dbh->rollback if $oldAutoCommit; - return $error if $error; + return $error if $error } $dbh->commit or die $dbh->errstr if $oldAutoCommit; @@ -278,6 +496,7 @@ sub check { $self->ut_numbern('svcnum') || $self->ut_numbern('pkgnum') || $self->ut_number('svcpart') + || $self->ut_numbern('agent_svcid') || $self->ut_numbern('overlimit') ; return $error if $error; @@ -285,30 +504,131 @@ sub check { 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. + if ( $self->pkgnum && ! $ignore_quantity ) { + + #slightly inefficient since ->pkg_svc will also look it up, but fixing + # a much larger perf problem and have bigger fish to fry + my $cust_pkg = $self->cust_pkg; + + my $pkg_svc = $self->pkg_svc + || new FS::pkg_svc { 'svcpart' => $self->svcpart, + 'pkgpart' => $cust_pkg->pkgpart, + 'quantity' => 0, + }; + + #service add-ons, kinda false laziness/reimplementation of part_pkg->pkg_svc + foreach my $part_pkg_link ( $cust_pkg->part_pkg->svc_part_pkg_link ) { + my $addon_pkg_svc = qsearchs('pkg_svc', { + pkgpart => $part_pkg_link->dst_pkgpart, + svcpart => $self->svcpart, + }); + $pkg_svc->quantity( $pkg_svc->quantity + $addon_pkg_svc->quantity ) + if $addon_pkg_svc; + } + + #better error message? UI shouldn't get here + return "No svcpart ". $self->svcpart. + " services in pkgpart ". $cust_pkg->pkgpart + unless $pkg_svc->quantity > 0; + + my $num_cust_svc = $cust_pkg->num_cust_svc( $self->svcpart ); + + #false laziness w/cust_pkg->part_svc + my $num_avail = max( 0, ($cust_pkg->quantity || 1) * $pkg_svc->quantity + - $num_cust_svc + ); + + #better error message? again, UI shouldn't get here + return "Already $num_cust_svc ". $pkg_svc->part_svc->svc. " services for pkgnum ". $self->pkgnum - if scalar(@cust_svc) >= $quantity && !$ignore_quantity; + if $num_avail <= 0; + + #part_svc_link rules (only make sense in pkgpart context, and + # skipping this when ignore_quantity is set DTRT when we're "forcing" + # an implicit change here (location change triggered pkgpart change, + # ->overlimit, bulk customer service changes) + foreach my $part_svc_link ( $self->part_svc_link( + link_type => 'cust_svc_provision_restrict', + ) + ) { + return $part_svc_link->dst_svc. ' must be provisioned before '. + $part_svc_link->src_svc + unless qsearchs({ + 'table' => 'cust_svc', + 'hashref' => { 'pkgnum' => $self->pkgnum, + 'svcpart' => $part_svc_link->dst_svcpart, + }, + 'order_by' => 'LIMIT 1', + }); + } + } $self->SUPER::check; } +=item check_part_svc_link_unprovision + +Checks service dependency unprovision rules for this service. + +If there is an error, returns the error, otherwise returns false. + +=cut + +sub check_part_svc_link_unprovision { + my $self = shift; + + foreach my $part_svc_link ( $self->part_svc_link( + link_type => 'cust_svc_unprovision_restrict', + ) + ) { + return $part_svc_link->dst_svc. ' must be unprovisioned before '. + $part_svc_link->src_svc + if qsearchs({ + 'table' => 'cust_svc', + 'hashref' => { 'pkgnum' => $self->pkgnum, + 'svcpart' => $part_svc_link->dst_svcpart, + }, + 'order_by' => 'LIMIT 1', + }); + } + + ''; +} + +=item part_svc_link + +Returns the service dependencies (see L) for the given +search options, taking into account this service definition as source and +this customer's agent. + +Available options are any field in part_svc_link. Typically used options are +link_type. + +=cut + +sub part_svc_link { + my $self = shift; + my $agentnum = $self->pkgnum ? $self->cust_pkg->cust_main->agentnum : ''; + FS::part_svc_link->by_agentnum($agentnum, + src_svcpart=>$self->svcpart, + disabled => '', + @_ + ); +} + +=item display_svcnum + +Returns the displayed service number for this service: agent_svcid if it has a +value, svcnum otherwise + +=cut + +sub display_svcnum { + my $self = shift; + $self->agent_svcid || $self->svcnum; +} + =item part_svc Returns the definition for this service, as a FS::part_svc object (see @@ -318,9 +638,9 @@ L). sub part_svc { my $self = shift; - $self->{'_svcpart'} - ? $self->{'_svcpart'} - : qsearchs( 'part_svc', { 'svcpart' => $self->svcpart } ); + return $self->{_svcpart} if $self->{_svcpart}; + cluck 'cust_svc->part_svc called' if $DEBUG; + qsearchs( 'part_svc', { 'svcpart' => $self->svcpart } ); } =item cust_pkg @@ -328,13 +648,6 @@ sub part_svc { Returns the package this service belongs to, as a FS::cust_pkg object (see L). -=cut - -sub cust_pkg { - my $self = shift; - qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } ); -} - =item pkg_svc Returns the pkg_svc record for for this service, if applicable. @@ -363,6 +676,20 @@ sub date_inserted { $self->h_date('insert'); } +=item pkg_cancel_date + +Returns the date this service's package was canceled. This normally only +exists for a service that's been preserved through cancellation with the +part_pkg.preserve flag. + +=cut + +sub pkg_cancel_date { + my $self = shift; + my $cust_pkg = $self->cust_pkg or return; + return $cust_pkg->getfield('cancel') || ''; +} + =item label Returns a list consisting of: @@ -411,18 +738,33 @@ sub _svc_label { =item export_links -Returns a list of html elements associated with this services exports. +Returns a listref of html elements associated with this service's exports. =cut sub export_links { my $self = shift; my $svc_x = $self->svc_x - or return "can't find ". $self->part_svc->svcdb. '.svcnum '. $self->svcnum; + or return [ "can't find ". $self->part_svc->svcdb. '.svcnum '. $self->svcnum ]; $svc_x->export_links; } +=item export_getsettings + +Returns two hashrefs of settings associated with this service's exports. + +=cut + +sub export_getsettings { + my $self = shift; + my $svc_x = $self->svc_x + or return "can't find ". $self->part_svc->svcdb. '.svcnum '. $self->svcnum; + + $svc_x->export_getsettings; +} + + =item svc_x Returns the FS::svc_XXX object for this service (i.e. an FS::svc_acct object or @@ -452,18 +794,20 @@ where B is not "svc_acct". =cut -#note: implementation here, POD in FS::svc_acct -sub seconds_since { - my($self, $since) = @_; - my $dbh = dbh; - my $sth = $dbh->prepare(' SELECT SUM(logout-login) FROM session - WHERE svcnum = ? - AND login >= ? - AND logout IS NOT NULL' - ) or die $dbh->errstr; - $sth->execute($self->svcnum, $since) or die $sth->errstr; - $sth->fetchrow_arrayref->[0]; -} +#internal session db deprecated (or at least on hold) +sub seconds_since { 'internal session db deprecated'; }; +##note: implementation here, POD in FS::svc_acct +#sub seconds_since { +# my($self, $since) = @_; +# my $dbh = dbh; +# my $sth = $dbh->prepare(' SELECT SUM(logout-login) FROM session +# WHERE svcnum = ? +# AND login >= ? +# AND logout IS NOT NULL' +# ) or die $dbh->errstr; +# $sth->execute($self->svcnum, $since) or die $sth->errstr; +# $sth->fetchrow_arrayref->[0]; +#} =item seconds_since_sqlradacct TIMESTAMP_START TIMESTAMP_END @@ -504,6 +848,7 @@ sub seconds_since_sqlradacct { #select a unix time conversion function based on database type my $str2time = str2time_sql( $dbh->{Driver}->{Name} ); + my $closing = str2time_sql_closing( $dbh->{Driver}->{Name} ); my $username = $part_export->export_username($svc_x); @@ -512,31 +857,46 @@ sub seconds_since_sqlradacct { warn "$mes finding closed sessions completely within the given range\n" if $DEBUG; + my $realm = ''; + my $realmparam = ''; + if ($part_export->option('process_single_realm')) { + $realm = 'AND Realm = ?'; + $realmparam = $part_export->option('realm'); + } + my $sth = $dbh->prepare("SELECT SUM(acctsessiontime) FROM radacct WHERE UserName = ? - AND $str2time AcctStartTime) >= ? - AND $str2time AcctStopTime ) < ? - AND $str2time AcctStopTime ) > 0 + $realm + AND $str2time AcctStartTime $closing >= ? + AND $str2time AcctStopTime $closing < ? + AND $str2time AcctStopTime $closing > 0 AND AcctStopTime IS NOT NULL" ) or die $dbh->errstr; - $sth->execute($username, $start, $end) or die $sth->errstr; + $sth->execute($username, ($realm ? $realmparam : ()), $start, $end) + or die $sth->errstr; my $regular = $sth->fetchrow_arrayref->[0]; warn "$mes finding open sessions which start in the range\n" if $DEBUG; # count session start->range end - $query = "SELECT SUM( ? - $str2time AcctStartTime ) ) + $query = "SELECT SUM( ? - $str2time AcctStartTime $closing ) FROM radacct WHERE UserName = ? - AND $str2time AcctStartTime ) >= ? - AND $str2time AcctStartTime ) < ? - AND ( ? - $str2time AcctStartTime ) ) < 86400 - AND ( $str2time AcctStopTime ) = 0 + $realm + AND $str2time AcctStartTime $closing >= ? + AND $str2time AcctStartTime $closing < ? + AND ( ? - $str2time AcctStartTime $closing ) < 86400 + AND ( $str2time AcctStopTime $closing = 0 OR AcctStopTime IS NULL )"; $sth = $dbh->prepare($query) or die $dbh->errstr; - $sth->execute($end, $username, $start, $end, $end) + $sth->execute( $end, + $username, + ($realm ? $realmparam : ()), + $start, + $end, + $end ) or die $sth->errstr. " executing query $query"; my $start_during = $sth->fetchrow_arrayref->[0]; @@ -544,16 +904,23 @@ sub seconds_since_sqlradacct { if $DEBUG; #count range start->session end - $sth = $dbh->prepare("SELECT SUM( $str2time AcctStopTime ) - ? ) + $sth = $dbh->prepare("SELECT SUM( $str2time AcctStopTime $closing - ? ) FROM radacct WHERE UserName = ? - AND $str2time AcctStartTime ) < ? - AND $str2time AcctStopTime ) >= ? - AND $str2time AcctStopTime ) < ? - AND $str2time AcctStopTime ) > 0 + $realm + AND $str2time AcctStartTime $closing < ? + AND $str2time AcctStopTime $closing >= ? + AND $str2time AcctStopTime $closing < ? + AND $str2time AcctStopTime $closing > 0 AND AcctStopTime IS NOT NULL" ) or die $dbh->errstr; - $sth->execute($start, $username, $start, $start, $end ) or die $sth->errstr; + $sth->execute( $start, + $username, + ($realm ? $realmparam : ()), + $start, + $start, + $end ) + or die $sth->errstr; my $end_during = $sth->fetchrow_arrayref->[0]; warn "$mes finding closed sessions which start before the range but stop after\n" @@ -564,13 +931,15 @@ sub seconds_since_sqlradacct { $sth = $dbh->prepare("SELECT COUNT(*) FROM radacct WHERE UserName = ? - AND $str2time AcctStartTime ) < ? - AND ( $str2time AcctStopTime ) >= ? + $realm + AND $str2time AcctStartTime $closing < ? + AND ( $str2time AcctStopTime $closing >= ? )" # OR AcctStopTime = 0 # OR AcctStopTime IS NULL )" ) or die $dbh->errstr; - $sth->execute($username, $start, $end ) or die $sth->errstr; + $sth->execute($username, ($realm ? $realmparam : ()), $start, $end ) + or die $sth->errstr; my $entire_range = ($end-$start) * $sth->fetchrow_arrayref->[0]; $seconds += $regular + $end_during + $start_during + $entire_range; @@ -625,20 +994,30 @@ sub attribute_since_sqlradacct { #select a unix time conversion function based on database type my $str2time = str2time_sql( $dbh->{Driver}->{Name} ); + my $closing = str2time_sql_closing( $dbh->{Driver}->{Name} ); my $username = $part_export->export_username($svc_x); warn "$mes SUMing $attrib sessions\n" if $DEBUG; + my $realm = ''; + my $realmparam = ''; + if ($part_export->option('process_single_realm')) { + $realm = 'AND Realm = ?'; + $realmparam = $part_export->option('realm'); + } + my $sth = $dbh->prepare("SELECT SUM($attrib) FROM radacct WHERE UserName = ? - AND $str2time AcctStopTime ) >= ? - AND $str2time AcctStopTime ) < ? + $realm + AND $str2time AcctStopTime $closing >= ? + AND $str2time AcctStopTime $closing < ? AND AcctStopTime IS NOT NULL" ) or die $dbh->errstr; - $sth->execute($username, $start, $end) or die $sth->errstr; + $sth->execute($username, ($realm ? $realmparam : ()), $start, $end) + or die $sth->errstr; my $row = $sth->fetchrow_arrayref; $sum += $row->[0] if defined($row->[0]); @@ -652,6 +1031,78 @@ sub attribute_since_sqlradacct { } +#note: implementation here, POD in FS::svc_acct +# false laziness w/above +sub attribute_last_sqlradacct { + my($self, $attrib) = @_; + + my $mes = "$me attribute_last_sqlradacct:"; + + my $svc_x = $self->svc_x; + + my @part_export = $self->part_svc->part_export_usage; + die "no accounting-capable exports are enabled for ". $self->part_svc->svc. + " service definition" + unless @part_export; + #or return undef; + + my $value = ''; + my $AcctStartTime = 0; + + foreach my $part_export ( @part_export ) { + + next if $part_export->option('ignore_accounting'); + + warn "$mes connecting to sqlradius database\n" + if $DEBUG; + + my $dbh = DBI->connect( map { $part_export->option($_) } + qw(datasrc username password) ) + or die "can't connect to sqlradius database: ". $DBI::errstr; + + warn "$mes connected to sqlradius database\n" + if $DEBUG; + + #select a unix time conversion function based on database type + my $str2time = str2time_sql( $dbh->{Driver}->{Name} ); + my $closing = str2time_sql_closing( $dbh->{Driver}->{Name} ); + + my $username = $part_export->export_username($svc_x); + + warn "$mes finding most-recent $attrib\n" + if $DEBUG; + + my $realm = ''; + my $realmparam = ''; + if ($part_export->option('process_single_realm')) { + $realm = 'AND Realm = ?'; + $realmparam = $part_export->option('realm'); + } + + my $sth = $dbh->prepare("SELECT $attrib, $str2time AcctStartTime $closing + FROM radacct + WHERE UserName = ? + $realm + ORDER BY AcctStartTime DESC LIMIT 1 + ") or die $dbh->errstr; + $sth->execute($username, ($realm ? $realmparam : ()) ) + or die $sth->errstr; + + my $row = $sth->fetchrow_arrayref; + if ( defined($row->[0]) && $row->[1] > $AcctStartTime ) { + $value = $row->[0]; + $AcctStartTime = $row->[1]; + } + + warn "$mes done\n" + if $DEBUG; + + } + + $value; + +} + =item get_session_history TIMESTAMP_START TIMESTAMP_END See L. Equivalent to @@ -682,45 +1133,202 @@ sub get_session_history { } -=item get_cdrs_for_update +=item tickets [ STATUS ] + +Returns an array of hashes representing the tickets linked to this service. + +An optional status (or arrayref or hashref of statuses) may be specified. + +=cut + +sub tickets { + my $self = shift; + my $status = ( @_ && $_[0] ) ? shift : ''; + + my $conf = FS::Conf->new; + my $num = $conf->config('cust_main-max_tickets') || 10; + my @tickets = (); + + if ( $conf->config('ticket_system') ) { + unless ( $conf->config('ticket_system-custom_priority_field') ) { + + @tickets = @{ FS::TicketSystem->service_tickets( $self->svcnum, + $num, + undef, + $status, + ) + }; + + } else { + + foreach my $priority ( + $conf->config('ticket_system-custom_priority_field-values'), '' + ) { + last if scalar(@tickets) >= $num; + push @tickets, + @{ FS::TicketSystem->service_tickets( $self->svcnum, + $num - scalar(@tickets), + $priority, + $status, + ) + }; + } + } + } + (@tickets); +} + +sub API_getinfo { + my $self = shift; + my $svc_x = $self->svc_x; + +{ ( map { $_=>$self->$_ } $self->fields ), + ( map { $svc_x=>$svc_x->$_ } $svc_x->fields ), + }; +} + +=back + +=head1 SUBROUTINES + +=over 4 -Returns (and SELECTs "FOR UPDATE") all unprocessed (freesidestatus NULL) CDR -objects (see L) associated with this service. +=item smart_search OPTION => VALUE ... -CDRs are associated with svc_phone services via svc_phone.phonenum +Accepts the option I, the string to search for. The string will +be searched for as a username, email address, IP address, MAC address, +phone number, and hardware serial number. Unlike the I on +customers, this always requires an exact match. =cut -sub get_cdrs_for_update { - my($self, %options) = @_; +# though perhaps it should be fuzzy in some cases? + +sub smart_search { + my %param = __PACKAGE__->smart_search_param(@_); + qsearch(\%param); +} + +sub smart_search_param { + my $class = shift; + my %opt = @_; - my @fields = ( 'charged_party' ); - push @fields, 'src' unless $options{'disable_src'}; + my $string = $opt{'search'}; + $string =~ s/(^\s+|\s+$)//; #trim leading & trailing whitespace - #CDRs are now associated with svc_phone services via svc_phone.phonenum - #return () unless $self->svc_x->isa('FS::svc_phone'); - return () unless $self->part_svc->svcdb eq 'svc_phone'; - my $number = $self->svc_x->phonenum; + my @or = + map { my $table = $_; + my $search_sql = "FS::$table"->search_sql($string); + my $addl_from = "FS::$table"->search_sql_addl_from(); - my $prefix = $options{'default_prefix'}; + "SELECT $table.svcnum AS svcnum, '$table' AS svcdb ". + "FROM $table $addl_from WHERE $search_sql"; + } + FS::part_svc->svc_tables; - my @where = map " $_ = '$number' ", @fields; - push @where, map " $_ = '$prefix$number' ", @fields - if length($prefix); - if ( $prefix =~ /^\+(\d+)$/ ) { - push @where, map " $_ = '$1$number' ", @fields + if ( $string =~ /^(\d+)$/ ) { + unshift @or, "SELECT cust_svc.svcnum, NULL as svcdb FROM cust_svc WHERE agent_svcid = $1"; } - my $extra_sql = ' AND ( '. join(' OR ', @where ). ' ) '; + my $addl_from = " RIGHT JOIN (\n" . join("\nUNION\n", @or) . "\n) AS svc_all ". + " ON (svc_all.svcnum = cust_svc.svcnum) "; + + my @extra_sql; + + push @extra_sql, $FS::CurrentUser::CurrentUser->agentnums_sql( + 'null_right' => 'View/link unlinked services' + ); + my $extra_sql = ' WHERE '.join(' AND ', @extra_sql); + #for agentnum + $addl_from .= ' LEFT JOIN cust_pkg USING ( pkgnum )'. + FS::UI::Web::join_cust_main('cust_pkg', 'cust_pkg'). + ' LEFT JOIN part_svc USING ( svcpart )'; + + ( + 'table' => 'cust_svc', + 'select' => 'svc_all.svcnum AS svcnum, '. + 'COALESCE(svc_all.svcdb, part_svc.svcdb) AS svcdb, '. + 'cust_svc.*', + 'addl_from' => $addl_from, + 'hashref' => {}, + 'extra_sql' => $extra_sql, + ); +} + +# If the associated cust_pkg is 'on hold' +# and the associated pkg_svc has the provision_hold flag +# and there are no more available_part_svcs on the cust_pkg similarly flagged, +# then removes hold from pkg +# returns $error or '' on success, +# does not indicate if pkg status was changed +sub _check_provision_hold { + my $self = shift; + + # check status of cust_pkg + my $cust_pkg = $self->cust_pkg; + return '' unless $cust_pkg && $cust_pkg->status eq 'on hold'; + + # check flag on this svc + # small false laziness with $self->pkg_svc + # to avoid looking up cust_pkg twice + my $pkg_svc = qsearchs( 'pkg_svc', { + 'svcpart' => $self->svcpart, + 'pkgpart' => $cust_pkg->pkgpart, + }); + return '' unless $pkg_svc->provision_hold; + + # check for any others available with that flag + return '' if $cust_pkg->available_part_svc( 'provision_hold' => 1 ); + + # conditions met, remove hold + return $cust_pkg->unsuspend; +} + +sub _upgrade_data { + my $class = shift; + + # fix missing (deleted by mistake) svc_x records + warn "searching for missing svc_x records...\n"; + my %search = ( + 'table' => 'cust_svc', + 'select' => 'cust_svc.*', + 'addl_from' => ' LEFT JOIN ( ' . + join(' UNION ', + map { "SELECT svcnum FROM $_" } + FS::part_svc->svc_tables + ) . ' ) AS svc_all ON cust_svc.svcnum = svc_all.svcnum', + 'extra_sql' => ' WHERE svc_all.svcnum IS NULL', + ); + my @svcs = qsearch(\%search); + warn "found ".scalar(@svcs)."\n"; - my @cdrs = - qsearch( { - 'table' => 'cdr', - 'hashref' => { 'freesidestatus' => '', }, - 'extra_sql' => "$extra_sql FOR UPDATE", - } ); + local $FS::Record::nowarn_classload = 1; # for h_svc_ + local $FS::svc_Common::noexport_hack = 1; # because we're inserting services + + my %h_search = ( + 'hashref' => { history_action => 'delete' }, + 'order_by' => ' ORDER BY history_date DESC LIMIT 1', + ); + foreach my $cust_svc (@svcs) { + my $svcnum = $cust_svc->svcnum; + my $svcdb = $cust_svc->part_svc->svcdb; + $h_search{'hashref'}{'svcnum'} = $svcnum; + $h_search{'table'} = "h_$svcdb"; + my $h_svc_x = qsearchs(\%h_search); + if ( $h_svc_x ) { + my $class = "FS::$svcdb"; + my $new_svc_x = $class->new({ $h_svc_x->hash }); + my $error = $new_svc_x->insert; + warn "error repairing svcnum $svcnum ($svcdb) from history:\n$error\n" + if $error; + } else { + # can't be fixed, so remove the dangling cust_svc to avoid breaking + # stuff + my $error = $cust_svc->delete; + warn "error cleaning up missing svcnum $svcnum ($svcdb):\n$error\n"; + } + } - @cdrs; + ''; } =back