1 package FS::part_export;
4 use vars qw( @ISA @EXPORT_OK $DEBUG %exports );
7 use base qw( FS::option_Common FS::m2m_Common );
8 use FS::Record qw( qsearch qsearchs dbh );
10 use FS::part_export_option;
11 use FS::part_export_machine;
12 use FS::svc_export_machine;
14 use FS::export_cust_svc;
16 #for export modules, though they should probably just use it themselves
19 @EXPORT_OK = qw(export_info);
25 FS::part_export - Object methods for part_export records
31 $record = new FS::part_export \%hash;
32 $record = new FS::part_export { 'column' => 'value' };
34 #($new_record, $options) = $template_recored->clone( $svcpart );
36 $error = $record->insert( { 'option' => 'value' } );
37 $error = $record->insert( \%options );
39 $error = $new_record->replace($old_record);
41 $error = $record->delete;
43 $error = $record->check;
47 An FS::part_export object represents an export of Freeside data to an external
48 provisioning system. FS::part_export inherits from FS::Record. The following
49 fields are currently supported:
53 =item exportnum - primary key
55 =item exportname - Descriptive name
57 =item machine - Machine name
59 =item exporttype - Export type
61 =item nodomain - blank or "Y" : usernames are exported to this service with no domain
63 =item default_machine - For exports that require a machine to be selected for
64 each service (see L<FS::svc_export_machine>), the one to use as the default.
66 =item no_suspend - Don't export service suspensions. In the future there may
67 be "no_*" options for the other service actions.
77 Creates a new export. To add the export to the database, see L<"insert">.
79 Note that this stores the hash reference, not a distinct copy of the hash it
80 points to. You can ask the object for a copy with the I<hash> method.
84 # the new method can be inherited from FS::Record, if a table method is defined
86 sub table { 'part_export'; }
92 #An alternate constructor. Creates a new export by duplicating an existing
93 #export. The given svcpart is assigned to the new export.
95 #Returns a list consisting of the new export object and a hashref of options.
101 # my $class = ref($self);
102 # my %hash = $self->hash;
103 # $hash{'exportnum'} = '';
104 # $hash{'svcpart'} = shift;
105 # ( $class->new( \%hash ),
106 # { map { $_->optionname => $_->optionvalue }
107 # qsearch('part_export_option', { 'exportnum' => $self->exportnum } )
114 Adds this record to the database. If there is an error, returns the error,
115 otherwise returns false.
117 If a hash reference of options is supplied, part_export_option records are
118 created (see L<FS::part_export_option>).
125 local $SIG{HUP} = 'IGNORE';
126 local $SIG{INT} = 'IGNORE';
127 local $SIG{QUIT} = 'IGNORE';
128 local $SIG{TERM} = 'IGNORE';
129 local $SIG{TSTP} = 'IGNORE';
130 local $SIG{PIPE} = 'IGNORE';
131 my $oldAutoCommit = $FS::UID::AutoCommit;
132 local $FS::UID::AutoCommit = 0;
135 my $error = $self->SUPER::insert(@_)
137 # use replace to do all the part_export_machine and default_machine stuff
139 $dbh->rollback if $oldAutoCommit;
143 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
149 Delete this record from the database.
153 #foreign keys would make this much less tedious... grr dumb mysql
157 local $SIG{HUP} = 'IGNORE';
158 local $SIG{INT} = 'IGNORE';
159 local $SIG{QUIT} = 'IGNORE';
160 local $SIG{TERM} = 'IGNORE';
161 local $SIG{TSTP} = 'IGNORE';
162 local $SIG{PIPE} = 'IGNORE';
163 my $oldAutoCommit = $FS::UID::AutoCommit;
164 local $FS::UID::AutoCommit = 0;
167 # delete associated export_cust_svc
168 foreach my $export_cust_svc (
169 qsearch('export_cust_svc',{ 'exportnum' => $self->exportnum })
171 my $error = $export_cust_svc->delete;
173 $dbh->rollback if $oldAutoCommit;
178 # clean up export_nas records
179 my $error = $self->process_m2m(
180 'link_table' => 'export_nas',
181 'target_table' => 'nas',
183 ) || $self->SUPER::delete;
185 $dbh->rollback if $oldAutoCommit;
189 foreach my $export_svc ( $self->export_svc ) {
190 my $error = $export_svc->delete;
192 $dbh->rollback if $oldAutoCommit;
197 foreach my $part_export_machine ( $self->part_export_machine ) {
198 my $error = $part_export_machine->delete;
200 $dbh->rollback if $oldAutoCommit;
205 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
209 =item replace [ OLD_RECORD ] [ HASHREF | OPTION => VALUE ... ]
211 Replaces the OLD_RECORD with this one in the database. If there is an error,
212 returns the error, otherwise returns false.
214 If a list or hash reference of options is supplied, option records are created
221 my $old = $self->replace_old;
223 local $SIG{HUP} = 'IGNORE';
224 local $SIG{INT} = 'IGNORE';
225 local $SIG{QUIT} = 'IGNORE';
226 local $SIG{TERM} = 'IGNORE';
227 local $SIG{TSTP} = 'IGNORE';
228 local $SIG{PIPE} = 'IGNORE';
230 my $oldAutoCommit = $FS::UID::AutoCommit;
231 local $FS::UID::AutoCommit = 0;
235 if ( $self->part_export_machine_textarea ) {
237 my %part_export_machine = map { $_->machine => $_ }
238 $self->part_export_machine;
240 my @machines = map { $_ =~ s/^\s+//; $_ =~ s/\s+$//; $_ }
243 $self->part_export_machine_textarea;
245 foreach my $machine ( @machines ) {
247 if ( $part_export_machine{$machine} ) {
249 if ( $part_export_machine{$machine}->disabled eq 'Y' ) {
250 $part_export_machine{$machine}->disabled('');
251 $error = $part_export_machine{$machine}->replace;
253 $dbh->rollback if $oldAutoCommit;
258 if ( $self->default_machine_name eq $machine ) {
259 $self->default_machine( $part_export_machine{$machine}->machinenum );
262 delete $part_export_machine{$machine}; #so we don't disable it below
266 my $part_export_machine = new FS::part_export_machine {
267 'exportnum' => $self->exportnum,
268 'machine' => $machine
270 $error = $part_export_machine->insert;
272 $dbh->rollback if $oldAutoCommit;
276 if ( $self->default_machine_name eq $machine ) {
277 $self->default_machine( $part_export_machine->machinenum );
283 foreach my $part_export_machine ( values %part_export_machine ) {
284 $part_export_machine->disabled('Y');
285 $error = $part_export_machine->replace;
287 $dbh->rollback if $oldAutoCommit;
292 if ( $old->machine ne '_SVC_MACHINE' ) {
293 # then set up the default for any already-attached export_svcs
294 foreach my $export_svc ( $self->export_svc ) {
295 my @svcs = qsearch('cust_svc', { 'svcpart' => $export_svc->svcpart });
296 foreach my $cust_svc ( @svcs ) {
297 my $svc_export_machine = FS::svc_export_machine->new({
298 'exportnum' => $self->exportnum,
299 'svcnum' => $cust_svc->svcnum,
300 'machinenum' => $self->default_machine,
302 $error ||= $svc_export_machine->insert;
306 $dbh->rollback if $oldAutoCommit;
309 } # if switching to selectable hosts
311 } elsif ( $old->machine eq '_SVC_MACHINE' ) {
312 # then we're switching from selectable to non-selectable
313 foreach my $svc_export_machine (
314 qsearch('svc_export_machine', { 'exportnum' => $self->exportnum })
316 $error ||= $svc_export_machine->delete;
319 $dbh->rollback if $oldAutoCommit;
325 $error = $self->SUPER::replace(@_);
327 $dbh->rollback if $oldAutoCommit;
331 if ( $self->machine eq '_SVC_MACHINE' and ! $self->default_machine ) {
332 $dbh->rollback if $oldAutoCommit;
333 return "no default export host selected";
336 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
342 Checks all fields to make sure this is a valid export. If there is
343 an error, returns the error, otherwise returns false. Called by the insert
351 $self->ut_numbern('exportnum')
352 || $self->ut_textn('exportname')
353 || $self->ut_domainn('machine')
354 || $self->ut_alpha('exporttype')
355 || $self->ut_flag('no_suspend')
358 if ( $self->machine eq '_SVC_MACHINE' ) {
359 $error ||= $self->ut_numbern('default_machine')
361 $self->set('default_machine', '');
364 return $error if $error;
366 $self->nodomain =~ /^(Y?)$/ or return "Illegal nodomain: ". $self->nodomain;
369 $self->deprecated(1); #BLAH
378 Returns a label for this export, "exportname||exportype (machine)".
384 ($self->exportname || $self->exporttype ). ' ('. $self->machine. ')';
389 Returns a label for this export, "exportname: exporttype to machine".
396 my $label = $self->exportname
397 ? '<B>'. $self->exportname. '</B>: ' #<BR>'.
400 $label .= $self->exporttype;
402 $label .= ' to '. ( $self->machine eq '_SVC_MACHINE'
403 ? 'per-service hostname'
414 #Returns the service definition (see L<FS::part_svc>) for this export.
420 # qsearchs('part_svc', { svcpart => $self->svcpart } );
425 croak "FS::part_export::part_svc deprecated";
426 #confess "FS::part_export::part_svc deprecated";
431 Returns a list of associated FS::svc_* records.
437 map { $_->svc_x } $self->cust_svc;
442 Returns a list of associated FS::cust_svc records.
448 map { qsearch('cust_svc', { 'svcpart' => $_->svcpart } ) }
449 grep { qsearch('cust_svc', { 'svcpart' => $_->svcpart } ) }
453 =item part_export_machine
455 Returns all machines as FS::part_export_machine objects (see
456 L<FS::part_export_machine>).
460 sub part_export_machine {
462 map { $_ } #behavior of sort undefined in scalar context
463 sort { $a->machine cmp $b->machine }
464 qsearch('part_export_machine', { 'exportnum' => $self->exportnum } );
469 Returns a list of associated FS::export_svc records.
475 qsearch('export_svc', { 'exportnum' => $self->exportnum } );
480 Returns a list of associated FS::export_device records.
486 qsearch('export_device', { 'exportnum' => $self->exportnum } );
489 =item part_export_option
491 Returns all options as FS::part_export_option objects (see
492 L<FS::part_export_option>).
496 sub part_export_option {
498 $self->option_objects;
503 Returns a list of option names and values suitable for assigning to a hash.
505 =item option OPTIONNAME
507 Returns the option value for the given name, or the empty string.
511 Reblesses the object into the FS::part_export::EXPORTTYPE class, where
512 EXPORTTYPE is the object's I<exporttype> field. There should be better docs
513 on how to create new exports, but until then, see L</NEW EXPORT CLASSES>.
519 my $exporttype = $self->exporttype;
520 my $class = ref($self). "::$exporttype";
523 bless($self, $class) unless $@;
527 =item svc_machine SVC_X
529 Return the export hostname for SVC_X.
534 my( $self, $svc_x ) = @_;
536 return $self->machine unless $self->machine eq '_SVC_MACHINE';
538 my $svc_export_machine = qsearchs('svc_export_machine', {
539 'svcnum' => $svc_x->svcnum,
540 'exportnum' => $self->exportnum,
543 if (!$svc_export_machine) {
544 warn "No hostname selected for ".($self->exportname || $self->exporttype);
545 return $self->default_export_machine->machine;
548 return $svc_export_machine->part_export_machine->machine;
551 =item default_export_machine
553 Return the default export hostname for this export.
557 sub default_export_machine {
559 my $machinenum = $self->default_machine;
561 my $default_machine = FS::part_export_machine->by_key($machinenum);
562 return $default_machine->machine if $default_machine;
564 # this should not happen
565 die "no default export hostname for export ".$self->exportnum;
568 #these should probably all go away, just let the subclasses define em
570 =item export_insert SVC_OBJECT
577 $self->_export_insert(@_);
583 # my $method = $AUTOLOAD;
584 # #$method =~ s/::(\w+)$/::_$1/; #infinite loop prevention
585 # $method =~ s/::(\w+)$/_$1/; #infinite loop prevention
586 # $self->$method(@_);
589 =item export_replace NEW OLD
596 $self->_export_replace(@_);
606 $self->_export_delete(@_);
616 $self->_export_suspend(@_);
619 =item export_unsuspend
623 sub export_unsuspend {
626 $self->_export_unsuspend(@_);
629 #fallbacks providing useful error messages intead of infinite loops
632 return "_export_insert: unknown export type ". $self->exporttype;
635 sub _export_replace {
637 return "_export_replace: unknown export type ". $self->exporttype;
642 return "_export_delete: unknown export type ". $self->exporttype;
645 #call svcdb-specific fallbacks
647 sub _export_suspend {
649 #warn "warning: _export_suspened unimplemented for". ref($self);
651 my $new = $svc_x->clone_suspended;
652 $self->_export_replace( $new, $svc_x );
655 sub _export_unsuspend {
657 #warn "warning: _export_unsuspend unimplemented for ". ref($self);
659 my $old = $svc_x->clone_kludge_unsuspend;
660 $self->_export_replace( $svc_x, $old );
663 =item get_remoteid SVC
665 Returns the remote id for this export for the given service.
670 my ($self, $svc_x) = @_;
672 my $export_cust_svc = qsearchs('export_cust_svc',{
673 'exportnum' => $self->exportnum,
674 'svcnum' => $svc_x->svcnum
677 return $export_cust_svc ? $export_cust_svc->remoteid : '';
680 =item set_remoteid SVC VALUE
682 Sets the remote id for this export for the given service.
683 See L<FS::export_cust_svc>.
685 If value is true, inserts or updates export_cust_svc record.
686 If value is false, deletes any existing record.
688 Returns error message, blank on success.
693 my ($self, $svc_x, $value) = @_;
695 my $export_cust_svc = qsearchs('export_cust_svc',{
696 'exportnum' => $self->exportnum,
697 'svcnum' => $svc_x->svcnum
700 local $SIG{HUP} = 'IGNORE';
701 local $SIG{INT} = 'IGNORE';
702 local $SIG{QUIT} = 'IGNORE';
703 local $SIG{TERM} = 'IGNORE';
704 local $SIG{TSTP} = 'IGNORE';
705 local $SIG{PIPE} = 'IGNORE';
707 my $oldAutoCommit = $FS::UID::AutoCommit;
708 local $FS::UID::AutoCommit = 0;
713 if ($export_cust_svc) {
714 $export_cust_svc->set('remoteid',$value);
715 $error = $export_cust_svc->replace;
717 $export_cust_svc = new FS::export_cust_svc {
718 'exportnum' => $self->exportnum,
719 'svcnum' => $svc_x->svcnum,
722 $error = $export_cust_svc->insert;
725 if ($export_cust_svc) {
726 $error = $export_cust_svc->delete;
727 } #otherwise, it already doesn't exist
730 if ($oldAutoCommit) {
731 $dbh->rollback if $error;
732 $dbh->commit unless $error;
738 =item export_links SVC_OBJECT ARRAYREF
740 Adds a list of web elements to ARRAYREF specific to this export and SVC_OBJECT.
741 The elements are displayed in the UI to lead the the operator to external
742 configuration, monitoring, and similar tools.
744 =item export_getsettings SVC_OBJECT SETTINGS_HASHREF DEFAUTS_HASHREF
746 Adds a hashref of settings to SETTINGSREF specific to this export and
747 SVC_OBJECT. The elements can be displayed in the UI on the service view.
749 DEFAULTSREF is a hashref with the same keys where true values indicate the
750 setting is a default (and thus can be displayed in the UI with less emphasis,
751 or hidden by default).
755 Adds one or more "action" links to the export's display in
756 browse/part_export.cgi. Should return pairs of values. The first is
757 the link label; the second is the Mason path to a document to load.
758 The document will show in a popup.
768 Returns the 'weight' element from the export's %info hash, or 0 if there is
775 export_info()->{$self->exporttype}->{'weight'} || 0;
780 Returns a reference to (a copy of) the export's %info hash.
787 %{ export_info()->{$self->exporttype} }
791 =item get_dids SELECTION
793 Does several things, which is unfortunate. DID phone numbers are organized
794 in a sort-of hierarchy: state, areacode, exchange, number. Or, for some
795 vendors: state, region, number. But not always that, either.
797 SELECTION is one or more field/value pairs specifying parts of the hierarchy
798 that have already been selected. C<get_dids> will then return an arrayref of
799 the possible values for the next selection level. Note that these are not
800 actual DIDs except at the lowest level.
802 Generally, 'state' alone will return an array of area codes or region names
805 'state' and 'areacode' together will return an array of either:
806 - exchange strings of the form "New York (212-555-XXXX)"
807 - ratecenter names of the form "New York, NY"
809 These strings are sent back to the UI and offered as options so that the user
810 can choose the local calling area they like.
812 'areacode' and 'exchange', or 'state' and 'ratecenter', or 'region' by itself
813 will return an array of actual DID numbers.
815 Passing 'tollfree' with a true value will override the whole hierarchy and
816 return an array of tollfree numbers.
818 C<get_dids> methods should report errors via die().
822 # no stub; can('get_dids') should return false by default
824 #default fallbacks... FS::part_export::DID_Common ?
825 sub get_dids_can_tollfree { 0; }
826 sub get_dids_can_manual { 0; }
827 sub get_dids_can_edit { 0; } #don't use without can_manual, otherwise the
828 # DID selector provisions a new number from
829 # inventory each edit
830 sub get_dids_npa_select { 1; }
832 # get_dids_npa_select: if true, then prompt to select state, then area code,
833 # then city/exchange, then phone number.
834 # if false, then prompt to select state (actually province), then "region",
837 # get_dids_can_manual: if true, then there will be a radio button to enter
838 # a phone number manually.
840 # get_dids_can_tollfree: if true, then the user will be prompted to choose
841 # both a regular and a toll-free number. The export can have a
842 # 'restrict_selection' option to enable only one or the other of those. See
843 # part_export/vitelity.pm for an example.
845 # get_dids_can_edit: if true, then the user can use the selector again to
846 # change the phone number for a service. if false, then they can't (have to
847 # reprovision completely).
851 Returns the role that SVC occupies with respect to this export, if any.
852 This is part of the part_svc's export configuration.
859 my $cust_svc = $svc_x->cust_svc or return '';
860 my $export_svc = qsearchs('export_svc', { exportnum => $self->exportnum,
861 svcpart => $cust_svc->svcpart })
866 =item svc_with_role { SVC | PKGNUM }, ROLE
868 Given a svc_* object SVC or pkgnum PKG, and a role name ROLE, finds the
869 service(s) in the same package that are linked to this export with ROLE.
875 my $svc_or_pkgnum = shift;
878 if ( ref $svc_or_pkgnum ) {
879 $pkgnum = $svc_or_pkgnum->cust_svc->pkgnum or return '';
881 $pkgnum = $svc_or_pkgnum;
883 my $role_info = $self->info->{roles}->{$role}
884 or die "role '$role' does not exist for export '".$self->exporttype."'\n";
885 my $svcdb = $role_info->{svcdb};
889 'addl_from' => ' JOIN cust_svc USING (svcnum)' .
890 ' JOIN export_svc USING (svcpart)',
891 'extra_sql' => " WHERE cust_svc.pkgnum = $pkgnum" .
892 " AND export_svc.exportnum = ".$self->exportnum .
893 " AND export_svc.role = '$role'",
895 if ( $role_info->{multiple} ) {
899 warn "multiple $role services in pkgnum $pkgnum; returning the first one.\n";
911 =item export_info [ SVCDB ]
913 Returns a hash reference of the exports for the given I<svcdb>, or if no
914 I<svcdb> is specified, for all exports. The keys of the hash are
915 I<exporttype>s and the values are again hash references containing information
918 'desc' => 'Description',
920 'option' => { label=>'Option Label' },
921 'option2' => { label=>'Another label' },
923 'nodomain' => 'Y', #or ''
924 'notes' => 'Additional notes',
930 return $exports{$_[0]} || {} if @_;
931 #{ map { %{$exports{$_}} } keys %exports };
932 my $r = { map { %{$exports{$_}} } keys %exports };
936 sub _upgrade_data { #class method
937 my ($class, %opts) = @_;
939 my @part_export_option = qsearch('part_export_option', { 'optionname' => 'overlimit_groups' });
940 foreach my $opt ( @part_export_option ) {
941 next if $opt->optionvalue =~ /^[\d\s]+$/ || !$opt->optionvalue;
942 my @groupnames = split(' ',$opt->optionvalue);
945 foreach my $groupname ( @groupnames ) {
946 my $g = qsearchs('radius_group', { 'groupname' => $groupname } );
948 $g = new FS::radius_group {
949 'groupname' => $groupname,
950 'description' => $groupname,
953 die $error if $error;
955 push @groupnums, $g->groupnum;
957 $opt->optionvalue(join(' ',@groupnums));
958 $error = $opt->replace;
959 die $error if $error;
961 # for exports that have selectable hostnames, make sure all services
962 # have a hostname selected
963 foreach my $part_export (
964 qsearch('part_export', { 'machine' => '_SVC_MACHINE' })
967 my $exportnum = $part_export->exportnum;
968 my $machinenum = $part_export->default_machine;
970 my ($first) = $part_export->part_export_machine;
972 # user intervention really is required.
973 die "Export $exportnum has no hostname options defined.\n".
974 "You must correct this before upgrading.\n";
976 # warn about this, because we might not choose the right one
977 warn "Export $exportnum (". $part_export->exporttype.
978 ") has no default hostname. Setting to ".$first->machine."\n";
979 $machinenum = $first->machinenum;
980 $part_export->set('default_machine', $machinenum);
981 my $error = $part_export->replace;
982 die $error if $error;
985 # the service belongs to a service def that uses this export
986 # and there is not a hostname selected for this export for that service
987 my $join = ' JOIN export_svc USING ( svcpart )'.
988 ' LEFT JOIN svc_export_machine'.
989 ' ON ( cust_svc.svcnum = svc_export_machine.svcnum'.
990 ' AND export_svc.exportnum = svc_export_machine.exportnum )';
992 my @svcs = qsearch( {
993 'select' => 'cust_svc.*',
994 'table' => 'cust_svc',
995 'addl_from' => $join,
996 'extra_sql' => ' WHERE svcexportmachinenum IS NULL'.
997 ' AND export_svc.exportnum = '.$part_export->exportnum,
999 foreach my $cust_svc (@svcs) {
1000 my $svc_export_machine = FS::svc_export_machine->new({
1001 'exportnum' => $exportnum,
1002 'machinenum' => $machinenum,
1003 'svcnum' => $cust_svc->svcnum,
1005 my $error = $svc_export_machine->insert;
1006 die $error if $error;
1012 $exports_in_use{ref $_} = 1 foreach qsearch('part_export', {});
1013 foreach (keys(%exports_in_use)) {
1014 $_->_upgrade_exporttype(%opts) if $_->can('_upgrade_exporttype');
1018 #=item exporttype2svcdb EXPORTTYPE
1020 #Returns the applicable I<svcdb> for an I<exporttype>.
1024 #sub exporttype2svcdb {
1025 # my $exporttype = $_[0];
1026 # foreach my $svcdb ( keys %exports ) {
1027 # return $svcdb if grep { $exporttype eq $_ } keys %{$exports{$svcdb}};
1032 #false laziness w/part_pkg & cdr
1033 foreach my $INC ( @INC ) {
1034 foreach my $file ( glob("$INC/FS/part_export/*.pm") ) {
1035 warn "attempting to load export info from $file\n" if $DEBUG;
1036 $file =~ /\/(\w+)\.pm$/ or do {
1037 warn "unrecognized file in $INC/FS/part_export/: $file\n";
1041 my $info = eval "use FS::part_export::$mod; ".
1042 "\\%FS::part_export::$mod\::info;";
1044 die "error using FS::part_export::$mod (skipping): $@\n" if $@;
1047 unless ( keys %$info ) {
1048 warn "no %info hash found in FS::part_export::$mod, skipping\n"
1049 unless $mod =~ /^(passwdfile|null|.+_Common)$/; #hack but what the heck
1052 warn "got export info from FS::part_export::$mod: $info\n" if $DEBUG;
1055 ref($info->{'svc'}) ? @{$info->{'svc'}} : $info->{'svc'}
1058 warn "blank svc for FS::part_export::$mod (skipping)\n";
1061 $exports{$svc}->{$mod} = $info;
1068 =head1 NEW EXPORT CLASSES
1070 A module should be added in FS/FS/part_export/ (an example may be found in
1071 eg/export_template.pm)
1075 Hmm... cust_export class (not necessarily a database table...) ... ?
1077 deprecated column...
1081 L<FS::part_export_option>, L<FS::export_svc>, L<FS::svc_acct>,
1083 L<FS::svc_forward>, L<FS::Record>, schema.html from the base documentation.