4 use vars qw( @ISA $DEBUG $me $ignore_quantity );
6 #use Scalar::Util qw( blessed );
8 use FS::Record qw( qsearch qsearchs dbh str2time_sql );
13 use FS::domain_record;
17 #most FS::svc_ classes are autoloaded in svc_x emthod
18 use FS::svc_acct; #this one is used in the cache stuff
20 @ISA = qw( FS::cust_main_Mixin FS::option_Common ); #FS::Record );
29 my ( $hashref, $cache ) = @_;
30 if ( $hashref->{'username'} ) {
31 $self->{'_svc_acct'} = FS::svc_acct->new($hashref, '');
33 if ( $hashref->{'svc'} ) {
34 $self->{'_svcpart'} = FS::part_svc->new($hashref);
40 FS::cust_svc - Object method for cust_svc objects
46 $record = new FS::cust_svc \%hash
47 $record = new FS::cust_svc { 'column' => 'value' };
49 $error = $record->insert;
51 $error = $new_record->replace($old_record);
53 $error = $record->delete;
55 $error = $record->check;
57 ($label, $value) = $record->label;
61 An FS::cust_svc represents a service. FS::cust_svc inherits from FS::Record.
62 The following fields are currently supported:
66 =item svcnum - primary key (assigned automatically for new services)
68 =item pkgnum - Package (see L<FS::cust_pkg>)
70 =item svcpart - Service definition (see L<FS::part_svc>)
72 =item overlimit - date the service exceeded its usage limit
82 Creates a new service. To add the refund to the database, see L<"insert">.
83 Services are normally created by creating FS::svc_ objects (see
84 L<FS::svc_acct>, L<FS::svc_domain>, and L<FS::svc_forward>, among others).
88 sub table { 'cust_svc'; }
92 Adds this service to the database. If there is an error, returns the error,
93 otherwise returns false.
97 Deletes this service from the database. If there is an error, returns the
98 error, otherwise returns false. Note that this only removes the cust_svc
99 record - you should probably use the B<cancel> method instead.
103 Cancels the relevant service by calling the B<cancel> method of the associated
104 FS::svc_XXX object (i.e. an FS::svc_acct object or FS::svc_domain object),
105 deleting the FS::svc_XXX record and then deleting this record.
107 If there is an error, returns the error, otherwise returns false.
114 local $SIG{HUP} = 'IGNORE';
115 local $SIG{INT} = 'IGNORE';
116 local $SIG{QUIT} = 'IGNORE';
117 local $SIG{TERM} = 'IGNORE';
118 local $SIG{TSTP} = 'IGNORE';
119 local $SIG{PIPE} = 'IGNORE';
121 my $oldAutoCommit = $FS::UID::AutoCommit;
122 local $FS::UID::AutoCommit = 0;
125 my $part_svc = $self->part_svc;
127 $part_svc->svcdb =~ /^([\w\-]+)$/ or do {
128 $dbh->rollback if $oldAutoCommit;
129 return "Illegal svcdb value in part_svc!";
132 require "FS/$svcdb.pm";
134 my $svc = $self->svc_x;
136 if ( %opt && $opt{'date'} ) {
137 my $error = $svc->expire($opt{'date'});
139 $dbh->rollback if $oldAutoCommit;
140 return "Error expiring service: $error";
143 my $error = $svc->cancel;
145 $dbh->rollback if $oldAutoCommit;
146 return "Error canceling service: $error";
148 $error = $svc->delete; #this deletes this cust_svc record as well
150 $dbh->rollback if $oldAutoCommit;
151 return "Error deleting service: $error";
158 warn "WARNING: no svc_ record found for svcnum ". $self->svcnum.
159 "; deleting cust_svc only\n";
161 my $error = $self->delete;
163 $dbh->rollback if $oldAutoCommit;
164 return "Error deleting cust_svc: $error";
169 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
175 =item overlimit [ ACTION ]
177 Retrieves or sets the overlimit date. If ACTION is absent, return
178 the present value of overlimit. If ACTION is present, it can
179 have the value 'suspend' or 'unsuspend'. In the case of 'suspend' overlimit
180 is set to the current time if it is not already set. The 'unsuspend' value
181 causes the time to be cleared.
183 If there is an error on setting, returns the error, otherwise returns false.
189 my $action = shift or return $self->getfield('overlimit');
191 local $SIG{HUP} = 'IGNORE';
192 local $SIG{INT} = 'IGNORE';
193 local $SIG{QUIT} = 'IGNORE';
194 local $SIG{TERM} = 'IGNORE';
195 local $SIG{TSTP} = 'IGNORE';
196 local $SIG{PIPE} = 'IGNORE';
198 my $oldAutoCommit = $FS::UID::AutoCommit;
199 local $FS::UID::AutoCommit = 0;
202 if ( $action eq 'suspend' ) {
203 $self->setfield('overlimit', time) unless $self->getfield('overlimit');
204 }elsif ( $action eq 'unsuspend' ) {
205 $self->setfield('overlimit', '');
207 die "unexpected action value: $action";
210 local $ignore_quantity = 1;
211 my $error = $self->replace;
213 $dbh->rollback if $oldAutoCommit;
214 return "Error setting overlimit: $error";
217 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
223 =item replace OLD_RECORD
225 Replaces the OLD_RECORD with this one in the database. If there is an error,
226 returns the error, otherwise returns false.
233 # my $old = ( blessed($_[0]) && $_[0]->isa('FS::Record') )
235 # : $new->replace_old;
236 my ( $new, $old ) = ( shift, shift );
237 $old = $new->replace_old unless defined($old);
239 local $SIG{HUP} = 'IGNORE';
240 local $SIG{INT} = 'IGNORE';
241 local $SIG{QUIT} = 'IGNORE';
242 local $SIG{TERM} = 'IGNORE';
243 local $SIG{TSTP} = 'IGNORE';
244 local $SIG{PIPE} = 'IGNORE';
246 my $oldAutoCommit = $FS::UID::AutoCommit;
247 local $FS::UID::AutoCommit = 0;
250 if ( $new->svcpart != $old->svcpart ) {
251 my $svc_x = $new->svc_x;
252 my $new_svc_x = ref($svc_x)->new({$svc_x->hash, svcpart=>$new->svcpart });
253 local($FS::Record::nowarn_identical) = 1;
254 my $error = $new_svc_x->replace($svc_x);
256 $dbh->rollback if $oldAutoCommit;
257 return $error if $error;
261 # #trigger a re-export on pkgnum changes?
262 # # (of prepaid packages), for Expiration RADIUS attribute
263 # if ( $new->pkgnum != $old->pkgnum && $new->cust_pkg->part_pkg->is_prepaid ) {
264 # my $svc_x = $new->svc_x;
265 # local($FS::Record::nowarn_identical) = 1;
266 # my $error = $svc_x->export('replace');
268 # $dbh->rollback if $oldAutoCommit;
269 # return $error if $error;
273 #my $error = $new->SUPER::replace($old, @_);
274 my $error = $new->SUPER::replace($old);
276 $dbh->rollback if $oldAutoCommit;
277 return $error if $error;
280 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
287 Checks all fields to make sure this is a valid service. If there is an error,
288 returns the error, otherwise returns false. Called by the insert and
297 $self->ut_numbern('svcnum')
298 || $self->ut_numbern('pkgnum')
299 || $self->ut_number('svcpart')
300 || $self->ut_numbern('overlimit')
302 return $error if $error;
304 my $part_svc = qsearchs( 'part_svc', { 'svcpart' => $self->svcpart } );
305 return "Unknown svcpart" unless $part_svc;
307 if ( $self->pkgnum ) {
308 my $cust_pkg = qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
309 return "Unknown pkgnum" unless $cust_pkg;
310 my $pkg_svc = qsearchs( 'pkg_svc', {
311 'pkgpart' => $cust_pkg->pkgpart,
312 'svcpart' => $self->svcpart,
314 # or new FS::pkg_svc ( { 'pkgpart' => $cust_pkg->pkgpart,
315 # 'svcpart' => $self->svcpart,
316 # 'quantity' => 0 } );
317 my $quantity = $pkg_svc ? $pkg_svc->quantity : 0;
319 my @cust_svc = qsearch('cust_svc', {
320 'pkgnum' => $self->pkgnum,
321 'svcpart' => $self->svcpart,
323 return "Already ". scalar(@cust_svc). " ". $part_svc->svc.
324 " services for pkgnum ". $self->pkgnum
325 if scalar(@cust_svc) >= $quantity && !$ignore_quantity;
333 Returns the definition for this service, as a FS::part_svc object (see
341 ? $self->{'_svcpart'}
342 : qsearchs( 'part_svc', { 'svcpart' => $self->svcpart } );
347 Returns the package this service belongs to, as a FS::cust_pkg object (see
354 qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
359 Returns the pkg_svc record for for this service, if applicable.
365 my $cust_pkg = $self->cust_pkg;
366 return undef unless $cust_pkg;
368 qsearchs( 'pkg_svc', { 'svcpart' => $self->svcpart,
369 'pkgpart' => $cust_pkg->pkgpart,
376 Returns the date this service was inserted.
382 $self->h_date('insert');
387 Returns a list consisting of:
388 - The name of this service (from part_svc)
389 - A meaningful identifier (username, domain, or mail alias)
390 - The table name (i.e. svc_domain) for this service
395 my($label, $value, $svcdb) = $cust_svc->label;
399 Like the B<label> method, except the second item in the list ("meaningful
400 identifier") may be longer - typically, a full name is included.
404 sub label { shift->_label('svc_label', @_); }
405 sub label_long { shift->_label('svc_label_long', @_); }
410 my $svc_x = $self->svc_x
411 or return "can't find ". $self->part_svc->svcdb. '.svcnum '. $self->svcnum;
413 $self->$method($svc_x);
416 sub svc_label { shift->_svc_label('label', @_); }
417 sub svc_label_long { shift->_svc_label('label_long', @_); }
420 my( $self, $method, $svc_x ) = ( shift, shift, shift );
423 $self->part_svc->svc,
425 $self->part_svc->svcdb,
433 Returns a listref of html elements associated with this service's exports.
439 my $svc_x = $self->svc_x
440 or return "can't find ". $self->part_svc->svcdb. '.svcnum '. $self->svcnum;
442 $svc_x->export_links;
445 =item export_getsettings
447 Returns two hashrefs of settings associated with this service's exports.
451 sub export_getsettings {
453 my $svc_x = $self->svc_x
454 or return "can't find ". $self->part_svc->svcdb. '.svcnum '. $self->svcnum;
456 $svc_x->export_getsettings;
462 Returns the FS::svc_XXX object for this service (i.e. an FS::svc_acct object or
463 FS::svc_domain object, etc.)
469 my $svcdb = $self->part_svc->svcdb;
470 if ( $svcdb eq 'svc_acct' && $self->{'_svc_acct'} ) {
471 $self->{'_svc_acct'};
473 require "FS/$svcdb.pm";
474 warn "$me svc_x: part_svc.svcpart ". $self->part_svc->svcpart.
475 ", so searching for $svcdb.svcnum ". $self->svcnum. "\n"
477 qsearchs( $svcdb, { 'svcnum' => $self->svcnum } );
481 =item seconds_since TIMESTAMP
483 See L<FS::svc_acct/seconds_since>. Equivalent to
484 $cust_svc->svc_x->seconds_since, but more efficient. Meaningless for records
485 where B<svcdb> is not "svc_acct".
489 #note: implementation here, POD in FS::svc_acct
491 my($self, $since) = @_;
493 my $sth = $dbh->prepare(' SELECT SUM(logout-login) FROM session
496 AND logout IS NOT NULL'
497 ) or die $dbh->errstr;
498 $sth->execute($self->svcnum, $since) or die $sth->errstr;
499 $sth->fetchrow_arrayref->[0];
502 =item seconds_since_sqlradacct TIMESTAMP_START TIMESTAMP_END
504 See L<FS::svc_acct/seconds_since_sqlradacct>. Equivalent to
505 $cust_svc->svc_x->seconds_since_sqlradacct, but more efficient. Meaningless
506 for records where B<svcdb> is not "svc_acct".
510 #note: implementation here, POD in FS::svc_acct
511 sub seconds_since_sqlradacct {
512 my($self, $start, $end) = @_;
514 my $mes = "$me seconds_since_sqlradacct:";
516 my $svc_x = $self->svc_x;
518 my @part_export = $self->part_svc->part_export_usage;
519 die "no accounting-capable exports are enabled for ". $self->part_svc->svc.
520 " service definition"
525 foreach my $part_export ( @part_export ) {
527 next if $part_export->option('ignore_accounting');
529 warn "$mes connecting to sqlradius database\n"
532 my $dbh = DBI->connect( map { $part_export->option($_) }
533 qw(datasrc username password) )
534 or die "can't connect to sqlradius database: ". $DBI::errstr;
536 warn "$mes connected to sqlradius database\n"
539 #select a unix time conversion function based on database type
540 my $str2time = str2time_sql( $dbh->{Driver}->{Name} );
542 my $username = $part_export->export_username($svc_x);
546 warn "$mes finding closed sessions completely within the given range\n"
551 if ($part_export->option('process_single_realm')) {
552 $realm = 'AND Realm = ?';
553 $realmparam = $part_export->option('realm');
556 my $sth = $dbh->prepare("SELECT SUM(acctsessiontime)
560 AND $str2time AcctStartTime) >= ?
561 AND $str2time AcctStopTime ) < ?
562 AND $str2time AcctStopTime ) > 0
563 AND AcctStopTime IS NOT NULL"
564 ) or die $dbh->errstr;
565 $sth->execute($username, ($realm ? $realmparam : ()), $start, $end)
567 my $regular = $sth->fetchrow_arrayref->[0];
569 warn "$mes finding open sessions which start in the range\n"
572 # count session start->range end
573 $query = "SELECT SUM( ? - $str2time AcctStartTime ) )
577 AND $str2time AcctStartTime ) >= ?
578 AND $str2time AcctStartTime ) < ?
579 AND ( ? - $str2time AcctStartTime ) ) < 86400
580 AND ( $str2time AcctStopTime ) = 0
581 OR AcctStopTime IS NULL )";
582 $sth = $dbh->prepare($query) or die $dbh->errstr;
585 ($realm ? $realmparam : ()),
589 or die $sth->errstr. " executing query $query";
590 my $start_during = $sth->fetchrow_arrayref->[0];
592 warn "$mes finding closed sessions which start before the range but stop during\n"
595 #count range start->session end
596 $sth = $dbh->prepare("SELECT SUM( $str2time AcctStopTime ) - ? )
600 AND $str2time AcctStartTime ) < ?
601 AND $str2time AcctStopTime ) >= ?
602 AND $str2time AcctStopTime ) < ?
603 AND $str2time AcctStopTime ) > 0
604 AND AcctStopTime IS NOT NULL"
605 ) or die $dbh->errstr;
606 $sth->execute( $start,
608 ($realm ? $realmparam : ()),
613 my $end_during = $sth->fetchrow_arrayref->[0];
615 warn "$mes finding closed sessions which start before the range but stop after\n"
618 # count range start->range end
619 # don't count open sessions anymore (probably missing stop record)
620 $sth = $dbh->prepare("SELECT COUNT(*)
624 AND $str2time AcctStartTime ) < ?
625 AND ( $str2time AcctStopTime ) >= ?
627 # OR AcctStopTime = 0
628 # OR AcctStopTime IS NULL )"
629 ) or die $dbh->errstr;
630 $sth->execute($username, ($realm ? $realmparam : ()), $start, $end )
632 my $entire_range = ($end-$start) * $sth->fetchrow_arrayref->[0];
634 $seconds += $regular + $end_during + $start_during + $entire_range;
636 warn "$mes done finding sessions\n"
645 =item attribute_since_sqlradacct TIMESTAMP_START TIMESTAMP_END ATTRIBUTE
647 See L<FS::svc_acct/attribute_since_sqlradacct>. Equivalent to
648 $cust_svc->svc_x->attribute_since_sqlradacct, but more efficient. Meaningless
649 for records where B<svcdb> is not "svc_acct".
653 #note: implementation here, POD in FS::svc_acct
654 #(false laziness w/seconds_since_sqlradacct above)
655 sub attribute_since_sqlradacct {
656 my($self, $start, $end, $attrib) = @_;
658 my $mes = "$me attribute_since_sqlradacct:";
660 my $svc_x = $self->svc_x;
662 my @part_export = $self->part_svc->part_export_usage;
663 die "no accounting-capable exports are enabled for ". $self->part_svc->svc.
664 " service definition"
670 foreach my $part_export ( @part_export ) {
672 next if $part_export->option('ignore_accounting');
674 warn "$mes connecting to sqlradius database\n"
677 my $dbh = DBI->connect( map { $part_export->option($_) }
678 qw(datasrc username password) )
679 or die "can't connect to sqlradius database: ". $DBI::errstr;
681 warn "$mes connected to sqlradius database\n"
684 #select a unix time conversion function based on database type
685 my $str2time = str2time_sql( $dbh->{Driver}->{Name} );
687 my $username = $part_export->export_username($svc_x);
689 warn "$mes SUMing $attrib sessions\n"
694 if ($part_export->option('process_single_realm')) {
695 $realm = 'AND Realm = ?';
696 $realmparam = $part_export->option('realm');
699 my $sth = $dbh->prepare("SELECT SUM($attrib)
703 AND $str2time AcctStopTime ) >= ?
704 AND $str2time AcctStopTime ) < ?
705 AND AcctStopTime IS NOT NULL"
706 ) or die $dbh->errstr;
707 $sth->execute($username, ($realm ? $realmparam : ()), $start, $end)
710 my $row = $sth->fetchrow_arrayref;
711 $sum += $row->[0] if defined($row->[0]);
713 warn "$mes done SUMing sessions\n"
722 =item get_session_history TIMESTAMP_START TIMESTAMP_END
724 See L<FS::svc_acct/get_session_history>. Equivalent to
725 $cust_svc->svc_x->get_session_history, but more efficient. Meaningless for
726 records where B<svcdb> is not "svc_acct".
730 sub get_session_history {
731 my($self, $start, $end, $attrib) = @_;
735 my @part_export = $self->part_svc->part_export_usage;
736 die "no accounting-capable exports are enabled for ". $self->part_svc->svc.
737 " service definition"
743 foreach my $part_export ( @part_export ) {
745 @{ $part_export->usage_sessions( $start, $end, $self->svc_x ) };
756 Behaviour of changing the svcpart of cust_svc records is undefined and should
757 possibly be prohibited, and pkg_svc records are not checked.
759 pkg_svc records are not checked in general (here).
761 Deleting this record doesn't check or delete the svc_* record associated
764 In seconds_since_sqlradacct, specifying a DATASRC/USERNAME/PASSWORD instead of
765 a DBI database handle is not yet implemented.
769 L<FS::Record>, L<FS::cust_pkg>, L<FS::part_svc>, L<FS::pkg_svc>,
770 schema.html from the base documentation