4 use vars qw( @ISA $DEBUG $me $ignore_quantity $conf $ticket_system );
6 #use Scalar::Util qw( blessed );
8 use FS::Record qw( qsearch qsearchs dbh str2time_sql );
13 use FS::domain_record;
18 #most FS::svc_ classes are autoloaded in svc_x emthod
19 use FS::svc_acct; #this one is used in the cache stuff
21 @ISA = qw( FS::cust_main_Mixin FS::option_Common ); #FS::Record );
28 #ask FS::UID to run this stuff for us later
29 FS::UID->install_callback( sub {
31 $ticket_system = $conf->config('ticket_system')
36 my ( $hashref, $cache ) = @_;
37 if ( $hashref->{'username'} ) {
38 $self->{'_svc_acct'} = FS::svc_acct->new($hashref, '');
40 if ( $hashref->{'svc'} ) {
41 $self->{'_svcpart'} = FS::part_svc->new($hashref);
47 FS::cust_svc - Object method for cust_svc objects
53 $record = new FS::cust_svc \%hash
54 $record = new FS::cust_svc { 'column' => 'value' };
56 $error = $record->insert;
58 $error = $new_record->replace($old_record);
60 $error = $record->delete;
62 $error = $record->check;
64 ($label, $value) = $record->label;
68 An FS::cust_svc represents a service. FS::cust_svc inherits from FS::Record.
69 The following fields are currently supported:
73 =item svcnum - primary key (assigned automatically for new services)
75 =item pkgnum - Package (see L<FS::cust_pkg>)
77 =item svcpart - Service definition (see L<FS::part_svc>)
79 =item agent_svcid - Optional legacy service ID
81 =item overlimit - date the service exceeded its usage limit
91 Creates a new service. To add the refund to the database, see L<"insert">.
92 Services are normally created by creating FS::svc_ objects (see
93 L<FS::svc_acct>, L<FS::svc_domain>, and L<FS::svc_forward>, among others).
97 sub table { 'cust_svc'; }
101 Adds this service to the database. If there is an error, returns the error,
102 otherwise returns false.
106 Deletes this service from the database. If there is an error, returns the
107 error, otherwise returns false. Note that this only removes the cust_svc
108 record - you should probably use the B<cancel> method instead.
116 my $error = $self->SUPER::delete;
117 return $error if $error;
119 if ( $ticket_system eq 'RT_Internal' ) {
120 unless ( $rt_session ) {
121 FS::TicketSystem->init;
122 $rt_session = FS::TicketSystem->session;
124 my $links = RT::Links->new($rt_session->{CurrentUser});
125 my $svcnum = $self->svcnum;
126 $links->Limit(FIELD => 'Target',
127 VALUE => 'freeside://freeside/cust_svc/'.$svcnum);
128 while ( my $l = $links->Next ) {
129 my ($val, $msg) = $l->Delete;
130 # can't do anything useful on error
131 warn "error unlinking ticket $svcnum: $msg\n" if !$val;
138 Cancels the relevant service by calling the B<cancel> method of the associated
139 FS::svc_XXX object (i.e. an FS::svc_acct object or FS::svc_domain object),
140 deleting the FS::svc_XXX record and then deleting this record.
142 If there is an error, returns the error, otherwise returns false.
149 local $SIG{HUP} = 'IGNORE';
150 local $SIG{INT} = 'IGNORE';
151 local $SIG{QUIT} = 'IGNORE';
152 local $SIG{TERM} = 'IGNORE';
153 local $SIG{TSTP} = 'IGNORE';
154 local $SIG{PIPE} = 'IGNORE';
156 my $oldAutoCommit = $FS::UID::AutoCommit;
157 local $FS::UID::AutoCommit = 0;
160 my $part_svc = $self->part_svc;
162 $part_svc->svcdb =~ /^([\w\-]+)$/ or do {
163 $dbh->rollback if $oldAutoCommit;
164 return "Illegal svcdb value in part_svc!";
167 require "FS/$svcdb.pm";
169 my $svc = $self->svc_x;
171 if ( %opt && $opt{'date'} ) {
172 my $error = $svc->expire($opt{'date'});
174 $dbh->rollback if $oldAutoCommit;
175 return "Error expiring service: $error";
178 my $error = $svc->cancel;
180 $dbh->rollback if $oldAutoCommit;
181 return "Error canceling service: $error";
183 $error = $svc->delete; #this deletes this cust_svc record as well
185 $dbh->rollback if $oldAutoCommit;
186 return "Error deleting service: $error";
193 warn "WARNING: no svc_ record found for svcnum ". $self->svcnum.
194 "; deleting cust_svc only\n";
196 my $error = $self->delete;
198 $dbh->rollback if $oldAutoCommit;
199 return "Error deleting cust_svc: $error";
204 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
210 =item overlimit [ ACTION ]
212 Retrieves or sets the overlimit date. If ACTION is absent, return
213 the present value of overlimit. If ACTION is present, it can
214 have the value 'suspend' or 'unsuspend'. In the case of 'suspend' overlimit
215 is set to the current time if it is not already set. The 'unsuspend' value
216 causes the time to be cleared.
218 If there is an error on setting, returns the error, otherwise returns false.
224 my $action = shift or return $self->getfield('overlimit');
226 local $SIG{HUP} = 'IGNORE';
227 local $SIG{INT} = 'IGNORE';
228 local $SIG{QUIT} = 'IGNORE';
229 local $SIG{TERM} = 'IGNORE';
230 local $SIG{TSTP} = 'IGNORE';
231 local $SIG{PIPE} = 'IGNORE';
233 my $oldAutoCommit = $FS::UID::AutoCommit;
234 local $FS::UID::AutoCommit = 0;
237 if ( $action eq 'suspend' ) {
238 $self->setfield('overlimit', time) unless $self->getfield('overlimit');
239 }elsif ( $action eq 'unsuspend' ) {
240 $self->setfield('overlimit', '');
242 die "unexpected action value: $action";
245 local $ignore_quantity = 1;
246 my $error = $self->replace;
248 $dbh->rollback if $oldAutoCommit;
249 return "Error setting overlimit: $error";
252 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
258 =item replace OLD_RECORD
260 Replaces the OLD_RECORD with this one in the database. If there is an error,
261 returns the error, otherwise returns false.
268 # my $old = ( blessed($_[0]) && $_[0]->isa('FS::Record') )
270 # : $new->replace_old;
271 my ( $new, $old ) = ( shift, shift );
272 $old = $new->replace_old unless defined($old);
274 local $SIG{HUP} = 'IGNORE';
275 local $SIG{INT} = 'IGNORE';
276 local $SIG{QUIT} = 'IGNORE';
277 local $SIG{TERM} = 'IGNORE';
278 local $SIG{TSTP} = 'IGNORE';
279 local $SIG{PIPE} = 'IGNORE';
281 my $oldAutoCommit = $FS::UID::AutoCommit;
282 local $FS::UID::AutoCommit = 0;
285 if ( $new->svcpart != $old->svcpart ) {
286 my $svc_x = $new->svc_x;
287 my $new_svc_x = ref($svc_x)->new({$svc_x->hash, svcpart=>$new->svcpart });
288 local($FS::Record::nowarn_identical) = 1;
289 my $error = $new_svc_x->replace($svc_x);
291 $dbh->rollback if $oldAutoCommit;
292 return $error if $error;
296 # #trigger a re-export on pkgnum changes?
297 # # (of prepaid packages), for Expiration RADIUS attribute
298 # if ( $new->pkgnum != $old->pkgnum && $new->cust_pkg->part_pkg->is_prepaid ) {
299 # my $svc_x = $new->svc_x;
300 # local($FS::Record::nowarn_identical) = 1;
301 # my $error = $svc_x->export('replace');
303 # $dbh->rollback if $oldAutoCommit;
304 # return $error if $error;
308 #trigger a pkg_change export on pkgnum changes
309 if ( $new->pkgnum != $old->pkgnum ) {
310 my $error = $new->svc_x->export('pkg_change', $new->cust_pkg,
314 $dbh->rollback if $oldAutoCommit;
315 return $error if $error;
319 #my $error = $new->SUPER::replace($old, @_);
320 my $error = $new->SUPER::replace($old);
322 $dbh->rollback if $oldAutoCommit;
323 return $error if $error;
326 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
333 Checks all fields to make sure this is a valid service. If there is an error,
334 returns the error, otherwise returns false. Called by the insert and
343 $self->ut_numbern('svcnum')
344 || $self->ut_numbern('pkgnum')
345 || $self->ut_number('svcpart')
346 || $self->ut_numbern('agent_svcid')
347 || $self->ut_numbern('overlimit')
349 return $error if $error;
351 my $part_svc = qsearchs( 'part_svc', { 'svcpart' => $self->svcpart } );
352 return "Unknown svcpart" unless $part_svc;
354 if ( $self->pkgnum && ! $ignore_quantity ) {
355 my $cust_pkg = qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
356 return "Unknown pkgnum" unless $cust_pkg;
357 ($part_svc) = grep { $_->svcpart == $self->svcpart } $cust_pkg->part_svc;
358 return "No svcpart ". $self->svcpart.
359 " services in pkgpart ". $cust_pkg->pkgpart
360 unless $part_svc || $ignore_quantity;
361 return "Already ". $part_svc->get('num_cust_svc'). " ". $part_svc->svc.
362 " services for pkgnum ". $self->pkgnum
363 if !$ignore_quantity && $part_svc->get('num_avail') <= 0 ;
371 Returns the displayed service number for this service: agent_svcid if it has a
372 value, svcnum otherwise
378 $self->agent_svcid || $self->svcnum;
383 Returns the definition for this service, as a FS::part_svc object (see
391 ? $self->{'_svcpart'}
392 : qsearchs( 'part_svc', { 'svcpart' => $self->svcpart } );
397 Returns the package this service belongs to, as a FS::cust_pkg object (see
404 qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
409 Returns the pkg_svc record for for this service, if applicable.
415 my $cust_pkg = $self->cust_pkg;
416 return undef unless $cust_pkg;
418 qsearchs( 'pkg_svc', { 'svcpart' => $self->svcpart,
419 'pkgpart' => $cust_pkg->pkgpart,
426 Returns the date this service was inserted.
432 $self->h_date('insert');
435 =item pkg_cancel_date
437 Returns the date this service's package was canceled. This normally only
438 exists for a service that's been preserved through cancellation with the
439 part_pkg.preserve flag.
443 sub pkg_cancel_date {
445 my $cust_pkg = $self->cust_pkg or return;
446 return $cust_pkg->getfield('cancel') || '';
451 Returns a list consisting of:
452 - The name of this service (from part_svc)
453 - A meaningful identifier (username, domain, or mail alias)
454 - The table name (i.e. svc_domain) for this service
459 my($label, $value, $svcdb) = $cust_svc->label;
463 Like the B<label> method, except the second item in the list ("meaningful
464 identifier") may be longer - typically, a full name is included.
468 sub label { shift->_label('svc_label', @_); }
469 sub label_long { shift->_label('svc_label_long', @_); }
474 my $svc_x = $self->svc_x
475 or return "can't find ". $self->part_svc->svcdb. '.svcnum '. $self->svcnum;
477 $self->$method($svc_x);
480 sub svc_label { shift->_svc_label('label', @_); }
481 sub svc_label_long { shift->_svc_label('label_long', @_); }
484 my( $self, $method, $svc_x ) = ( shift, shift, shift );
487 $self->part_svc->svc,
489 $self->part_svc->svcdb,
497 Returns a listref of html elements associated with this service's exports.
503 my $svc_x = $self->svc_x
504 or return [ "can't find ". $self->part_svc->svcdb. '.svcnum '. $self->svcnum ];
506 $svc_x->export_links;
509 =item export_getsettings
511 Returns two hashrefs of settings associated with this service's exports.
515 sub export_getsettings {
517 my $svc_x = $self->svc_x
518 or return "can't find ". $self->part_svc->svcdb. '.svcnum '. $self->svcnum;
520 $svc_x->export_getsettings;
526 Returns the FS::svc_XXX object for this service (i.e. an FS::svc_acct object or
527 FS::svc_domain object, etc.)
533 my $svcdb = $self->part_svc->svcdb;
534 if ( $svcdb eq 'svc_acct' && $self->{'_svc_acct'} ) {
535 $self->{'_svc_acct'};
537 require "FS/$svcdb.pm";
538 warn "$me svc_x: part_svc.svcpart ". $self->part_svc->svcpart.
539 ", so searching for $svcdb.svcnum ". $self->svcnum. "\n"
541 qsearchs( $svcdb, { 'svcnum' => $self->svcnum } );
545 =item seconds_since TIMESTAMP
547 See L<FS::svc_acct/seconds_since>. Equivalent to
548 $cust_svc->svc_x->seconds_since, but more efficient. Meaningless for records
549 where B<svcdb> is not "svc_acct".
553 #internal session db deprecated (or at least on hold)
554 sub seconds_since { 'internal session db deprecated'; };
555 ##note: implementation here, POD in FS::svc_acct
557 # my($self, $since) = @_;
559 # my $sth = $dbh->prepare(' SELECT SUM(logout-login) FROM session
562 # AND logout IS NOT NULL'
563 # ) or die $dbh->errstr;
564 # $sth->execute($self->svcnum, $since) or die $sth->errstr;
565 # $sth->fetchrow_arrayref->[0];
568 =item seconds_since_sqlradacct TIMESTAMP_START TIMESTAMP_END
570 See L<FS::svc_acct/seconds_since_sqlradacct>. Equivalent to
571 $cust_svc->svc_x->seconds_since_sqlradacct, but more efficient. Meaningless
572 for records where B<svcdb> is not "svc_acct".
576 #note: implementation here, POD in FS::svc_acct
577 sub seconds_since_sqlradacct {
578 my($self, $start, $end) = @_;
580 my $mes = "$me seconds_since_sqlradacct:";
582 my $svc_x = $self->svc_x;
584 my @part_export = $self->part_svc->part_export_usage;
585 die "no accounting-capable exports are enabled for ". $self->part_svc->svc.
586 " service definition"
591 foreach my $part_export ( @part_export ) {
593 next if $part_export->option('ignore_accounting');
595 warn "$mes connecting to sqlradius database\n"
598 my $dbh = DBI->connect( map { $part_export->option($_) }
599 qw(datasrc username password) )
600 or die "can't connect to sqlradius database: ". $DBI::errstr;
602 warn "$mes connected to sqlradius database\n"
605 #select a unix time conversion function based on database type
606 my $str2time = str2time_sql( $dbh->{Driver}->{Name} );
608 my $username = $part_export->export_username($svc_x);
612 warn "$mes finding closed sessions completely within the given range\n"
617 if ($part_export->option('process_single_realm')) {
618 $realm = 'AND Realm = ?';
619 $realmparam = $part_export->option('realm');
622 my $sth = $dbh->prepare("SELECT SUM(acctsessiontime)
626 AND $str2time AcctStartTime) >= ?
627 AND $str2time AcctStopTime ) < ?
628 AND $str2time AcctStopTime ) > 0
629 AND AcctStopTime IS NOT NULL"
630 ) or die $dbh->errstr;
631 $sth->execute($username, ($realm ? $realmparam : ()), $start, $end)
633 my $regular = $sth->fetchrow_arrayref->[0];
635 warn "$mes finding open sessions which start in the range\n"
638 # count session start->range end
639 $query = "SELECT SUM( ? - $str2time AcctStartTime ) )
643 AND $str2time AcctStartTime ) >= ?
644 AND $str2time AcctStartTime ) < ?
645 AND ( ? - $str2time AcctStartTime ) ) < 86400
646 AND ( $str2time AcctStopTime ) = 0
647 OR AcctStopTime IS NULL )";
648 $sth = $dbh->prepare($query) or die $dbh->errstr;
651 ($realm ? $realmparam : ()),
655 or die $sth->errstr. " executing query $query";
656 my $start_during = $sth->fetchrow_arrayref->[0];
658 warn "$mes finding closed sessions which start before the range but stop during\n"
661 #count range start->session end
662 $sth = $dbh->prepare("SELECT SUM( $str2time AcctStopTime ) - ? )
666 AND $str2time AcctStartTime ) < ?
667 AND $str2time AcctStopTime ) >= ?
668 AND $str2time AcctStopTime ) < ?
669 AND $str2time AcctStopTime ) > 0
670 AND AcctStopTime IS NOT NULL"
671 ) or die $dbh->errstr;
672 $sth->execute( $start,
674 ($realm ? $realmparam : ()),
679 my $end_during = $sth->fetchrow_arrayref->[0];
681 warn "$mes finding closed sessions which start before the range but stop after\n"
684 # count range start->range end
685 # don't count open sessions anymore (probably missing stop record)
686 $sth = $dbh->prepare("SELECT COUNT(*)
690 AND $str2time AcctStartTime ) < ?
691 AND ( $str2time AcctStopTime ) >= ?
693 # OR AcctStopTime = 0
694 # OR AcctStopTime IS NULL )"
695 ) or die $dbh->errstr;
696 $sth->execute($username, ($realm ? $realmparam : ()), $start, $end )
698 my $entire_range = ($end-$start) * $sth->fetchrow_arrayref->[0];
700 $seconds += $regular + $end_during + $start_during + $entire_range;
702 warn "$mes done finding sessions\n"
711 =item attribute_since_sqlradacct TIMESTAMP_START TIMESTAMP_END ATTRIBUTE
713 See L<FS::svc_acct/attribute_since_sqlradacct>. Equivalent to
714 $cust_svc->svc_x->attribute_since_sqlradacct, but more efficient. Meaningless
715 for records where B<svcdb> is not "svc_acct".
719 #note: implementation here, POD in FS::svc_acct
720 #(false laziness w/seconds_since_sqlradacct above)
721 sub attribute_since_sqlradacct {
722 my($self, $start, $end, $attrib) = @_;
724 my $mes = "$me attribute_since_sqlradacct:";
726 my $svc_x = $self->svc_x;
728 my @part_export = $self->part_svc->part_export_usage;
729 die "no accounting-capable exports are enabled for ". $self->part_svc->svc.
730 " service definition"
736 foreach my $part_export ( @part_export ) {
738 next if $part_export->option('ignore_accounting');
740 warn "$mes connecting to sqlradius database\n"
743 my $dbh = DBI->connect( map { $part_export->option($_) }
744 qw(datasrc username password) )
745 or die "can't connect to sqlradius database: ". $DBI::errstr;
747 warn "$mes connected to sqlradius database\n"
750 #select a unix time conversion function based on database type
751 my $str2time = str2time_sql( $dbh->{Driver}->{Name} );
753 my $username = $part_export->export_username($svc_x);
755 warn "$mes SUMing $attrib sessions\n"
760 if ($part_export->option('process_single_realm')) {
761 $realm = 'AND Realm = ?';
762 $realmparam = $part_export->option('realm');
765 my $sth = $dbh->prepare("SELECT SUM($attrib)
769 AND $str2time AcctStopTime ) >= ?
770 AND $str2time AcctStopTime ) < ?
771 AND AcctStopTime IS NOT NULL"
772 ) or die $dbh->errstr;
773 $sth->execute($username, ($realm ? $realmparam : ()), $start, $end)
776 my $row = $sth->fetchrow_arrayref;
777 $sum += $row->[0] if defined($row->[0]);
779 warn "$mes done SUMing sessions\n"
788 =item get_session_history TIMESTAMP_START TIMESTAMP_END
790 See L<FS::svc_acct/get_session_history>. Equivalent to
791 $cust_svc->svc_x->get_session_history, but more efficient. Meaningless for
792 records where B<svcdb> is not "svc_acct".
796 sub get_session_history {
797 my($self, $start, $end, $attrib) = @_;
801 my @part_export = $self->part_svc->part_export_usage;
802 die "no accounting-capable exports are enabled for ". $self->part_svc->svc.
803 " service definition"
809 foreach my $part_export ( @part_export ) {
811 @{ $part_export->usage_sessions( $start, $end, $self->svc_x ) };
818 =item tickets [ STATUS ]
820 Returns an array of hashes representing the tickets linked to this service.
822 An optional status (or arrayref or hashref of statuses) may be specified.
828 my $status = ( @_ && $_[0] ) ? shift : '';
830 my $conf = FS::Conf->new;
831 my $num = $conf->config('cust_main-max_tickets') || 10;
834 if ( $conf->config('ticket_system') ) {
835 unless ( $conf->config('ticket_system-custom_priority_field') ) {
837 @tickets = @{ FS::TicketSystem->service_tickets( $self->svcnum,
846 foreach my $priority (
847 $conf->config('ticket_system-custom_priority_field-values'), ''
849 last if scalar(@tickets) >= $num;
851 @{ FS::TicketSystem->service_tickets( $self->svcnum,
852 $num - scalar(@tickets),
870 =item smart_search OPTION => VALUE ...
872 Accepts the option I<search>, the string to search for. The string will
873 be searched for as a username, email address, IP address, MAC address,
874 phone number, and hardware serial number. Unlike the I<smart_search> on
875 customers, this always requires an exact match.
879 # though perhaps it should be fuzzy in some cases?
882 my %param = __PACKAGE__->smart_search_param(@_);
886 sub smart_search_param {
890 my $string = $opt{'search'};
891 $string =~ s/(^\s+|\s+$)//; #trim leading & trailing whitespace
894 map { my $table = $_;
895 my $search_sql = "FS::$table"->search_sql($string);
897 "SELECT $table.svcnum AS svcnum, '$table' AS svcdb ".
898 "FROM $table WHERE $search_sql";
900 FS::part_svc->svc_tables;
902 if ( $string =~ /^(\d+)$/ ) {
903 unshift @or, "SELECT cust_svc.svcnum, NULL as svcdb FROM cust_svc WHERE agent_svcid = $1";
906 my $addl_from = " RIGHT JOIN (\n" . join("\nUNION\n", @or) . "\n) AS svc_all ".
907 " ON (svc_all.svcnum = cust_svc.svcnum) ";
911 push @extra_sql, $FS::CurrentUser::CurrentUser->agentnums_sql(
912 'null_right' => 'View/link unlinked services'
914 my $extra_sql = ' WHERE '.join(' AND ', @extra_sql);
916 $addl_from .= ' LEFT JOIN cust_pkg USING ( pkgnum )'.
917 FS::UI::Web::join_cust_main('cust_pkg', 'cust_pkg').
918 ' LEFT JOIN part_svc USING ( svcpart )';
921 'table' => 'cust_svc',
922 'select' => 'svc_all.svcnum AS svcnum, '.
923 'COALESCE(svc_all.svcdb, part_svc.svcdb) AS svcdb, '.
925 'addl_from' => $addl_from,
927 'extra_sql' => $extra_sql,
934 # fix missing (deleted by mistake) svc_x records
935 warn "searching for missing svc_x records...\n";
937 'table' => 'cust_svc',
938 'select' => 'cust_svc.*',
939 'addl_from' => ' LEFT JOIN ( ' .
941 map { "SELECT svcnum FROM $_" }
942 FS::part_svc->svc_tables
943 ) . ' ) AS svc_all ON cust_svc.svcnum = svc_all.svcnum',
944 'extra_sql' => ' WHERE svc_all.svcnum IS NULL',
946 my @svcs = qsearch(\%search);
947 warn "found ".scalar(@svcs)."\n";
949 local $FS::Record::nowarn_classload = 1; # for h_svc_
950 local $FS::svc_Common::noexport_hack = 1; # because we're inserting services
953 'hashref' => { history_action => 'delete' },
954 'order_by' => ' ORDER BY history_date DESC LIMIT 1',
956 foreach my $cust_svc (@svcs) {
957 my $svcnum = $cust_svc->svcnum;
958 my $svcdb = $cust_svc->part_svc->svcdb;
959 $h_search{'hashref'}{'svcnum'} = $svcnum;
960 $h_search{'table'} = "h_$svcdb";
961 my $h_svc_x = qsearchs(\%h_search)
963 my $class = "FS::$svcdb";
964 my $new_svc_x = $class->new({ $h_svc_x->hash });
965 my $error = $new_svc_x->insert;
966 warn "error repairing svcnum $svcnum ($svcdb) from history:\n$error\n"
977 Behaviour of changing the svcpart of cust_svc records is undefined and should
978 possibly be prohibited, and pkg_svc records are not checked.
980 pkg_svc records are not checked in general (here).
982 Deleting this record doesn't check or delete the svc_* record associated
985 In seconds_since_sqlradacct, specifying a DATASRC/USERNAME/PASSWORD instead of
986 a DBI database handle is not yet implemented.
990 L<FS::Record>, L<FS::cust_pkg>, L<FS::part_svc>, L<FS::pkg_svc>,
991 schema.html from the base documentation