#use Scalar::Util qw( blessed );
use List::Util qw( max );
use FS::Conf;
-use FS::Record qw( qsearch qsearchs dbh str2time_sql );
+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;
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
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;
- return $error if $error;
+ 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 ) {
warn "error unlinking ticket $svcnum: $msg\n" if !$val;
}
}
+
+ '';
+
+}
+
+=item suspend
+
+Suspends the relevant service by calling the B<suspend> 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
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;
my $cust_pkg = $self->cust_pkg;
my $pkg_svc = $self->pkg_svc
- or return "No svcpart ". $self->svcpart.
- " services in pkgpart ". $cust_pkg->pkgpart;
+ || 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 );
- $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 $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<FS::part_svc_link>) 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, @_);
+}
+
=item display_svcnum
Returns the displayed service number for this service: agent_svcid if it has a
#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);
FROM radacct
WHERE UserName = ?
$realm
- AND $str2time AcctStartTime) >= ?
- AND $str2time AcctStopTime ) < ?
- AND $str2time AcctStopTime ) > 0
+ 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, ($realm ? $realmparam : ()), $start, $end)
if $DEBUG;
# count session start->range end
- $query = "SELECT SUM( ? - $str2time AcctStartTime ) )
+ $query = "SELECT SUM( ? - $str2time AcctStartTime $closing )
FROM radacct
WHERE UserName = ?
$realm
- AND $str2time AcctStartTime ) >= ?
- AND $str2time AcctStartTime ) < ?
- AND ( ? - $str2time AcctStartTime ) ) < 86400
- AND ( $str2time AcctStopTime ) = 0
+ 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,
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 = ?
$realm
- AND $str2time AcctStartTime ) < ?
- AND $str2time AcctStopTime ) >= ?
- AND $str2time AcctStopTime ) < ?
- AND $str2time AcctStopTime ) > 0
+ 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,
FROM radacct
WHERE UserName = ?
$realm
- AND $str2time AcctStartTime ) < ?
- AND ( $str2time AcctStopTime ) >= ?
+ AND $str2time AcctStartTime $closing < ?
+ AND ( $str2time AcctStopTime $closing >= ?
)"
# OR AcctStopTime = 0
# OR AcctStopTime IS NULL )"
#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);
FROM radacct
WHERE UserName = ?
$realm
- AND $str2time AcctStopTime ) >= ?
- AND $str2time AcctStopTime ) < ?
+ AND $str2time AcctStopTime $closing >= ?
+ AND $str2time AcctStopTime $closing < ?
AND AcctStopTime IS NOT NULL"
) or die $dbh->errstr;
$sth->execute($username, ($realm ? $realmparam : ()), $start, $end)
}
+#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<FS::svc_acct/get_session_history>. Equivalent to
(@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
my @or =
map { my $table = $_;
my $search_sql = "FS::$table"->search_sql($string);
+ my $addl_from = "FS::$table"->search_sql_addl_from();
"SELECT $table.svcnum AS svcnum, '$table' AS svcdb ".
- "FROM $table WHERE $search_sql";
+ "FROM $table $addl_from WHERE $search_sql";
}
FS::part_svc->svc_tables;
);
}
+# 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->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;