diff options
Diffstat (limited to 'FS')
116 files changed, 11426 insertions, 3418 deletions
@@ -54,6 +54,8 @@ L<FS::svc_Common> - Service base class L<FS::svc_acct> - Account (shell, RADIUS, POP3) class +L<FS::acct_snarf> - External mail account class + L<FS::radius_usergroup> - RADIUS groups L<FS::svc_domain> - Domain class @@ -62,10 +64,12 @@ L<FS::domain_record> - DNS zone entries L<FS::svc_forward> - Mail forwarding class -L<FS::svc_acct_sm> - (Depreciated) Vitual mail alias class - L<FS::svc_www> - Web virtual host class. +L<FS::svc_broadband> - DSL, wireless and other broadband class. + +L<FS::svc_external> - Externally tracked service class. + L<FS::part_svc> - Service definition class L<FS::part_svc_column> - Column constraint class @@ -104,6 +108,8 @@ L<FS::cust_bill> - Invoice class L<FS::cust_bill_pkg> - Invoice line item class +L<FS::cust_bill_pkg_detail> - Invoice line item detail class + L<FS::part_bill_event> - Invoice event definition class L<FS::cust_bill_event> - Completed invoice event class @@ -187,7 +193,7 @@ first time, the suggested order will tend to reduce the number of forward references." If you've never used OO modules before, -http://www.cpan.org/doc/FMTEYEWTK/easy_objects.html might help you out. +http://www.perl.com/doc/FMTEYEWTK/easy_objects.html might help you out. =head1 DESCRIPTION diff --git a/FS/FS/CGI.pm b/FS/FS/CGI.pm index e44ebcc0a..905189e2e 100644 --- a/FS/FS/CGI.pm +++ b/FS/FS/CGI.pm @@ -10,7 +10,7 @@ use FS::UID; @ISA = qw(Exporter); @EXPORT_OK = qw(header menubar idiot eidiot popurl table itable ntable - small_custview myexit); + small_custview myexit http_header); =head1 NAME @@ -44,8 +44,10 @@ Returns an HTML header. =cut sub header { + use Carp; + carp 'FS::CGI::header deprecated; include /elements/header.html instead'; + my($title,$menubar,$etc)=@_; #$etc is for things like onLoad= etc. - #use Carp; $etc = '' unless defined $etc; my $x = <<END; @@ -68,6 +70,38 @@ END $x; } +=item http_header + +Sets an http header. + +=cut + +sub http_header { + my ( $header, $value ) = @_; + if (exists $ENV{MOD_PERL}) { + if ( defined $main::Response + && $main::Response->isa('Apache::ASP::Response') ) { #Apache::ASP + if ( $header =~ /^Content-Type$/ ) { + $main::Response->{ContentType} = $value; + } else { + $main::Response->AddHeader( $header => $value ); + } + } elsif ( defined $HTML::Mason::Commands::r ) { #Mason + ## is this the correct pacakge for $r ??? for 1.0x and 1.1x ? + if ( $header =~ /^Content-Type$/ ) { + $HTML::Mason::Commands::r->content_type($value); + } else { + $HTML::Mason::Commands::r->header_out( $header => $value ); + } + } else { + die "http_header called in unknown environment"; + } + } else { + die "http_header called not running under mod_perl"; + } + +} + =item menubar ITEM, URL, ... Returns an HTML menubar. @@ -75,6 +109,9 @@ Returns an HTML menubar. =cut sub menubar { #$menubar=menubar('Main Menu', '../', 'Item', 'url', ... ); + use Carp; + carp 'FS::CGI::menubar deprecated; include /elements/menubar.html instead'; + my($item,$url,@html); while (@_) { ($item,$url)=splice(@_,0,2); @@ -177,7 +214,9 @@ Returns current URL with LEVEL levels of path removed from the end (default 0). sub popurl { my($up)=@_; my $cgi = &FS::UID::cgi; - my $url = new URI::URL ( $cgi->isa('Apache') ? $cgi->uri : $cgi->url ); + my $url_string = $cgi->isa('Apache') ? $cgi->uri : $cgi->url; + $url_string =~ s/\?.*//; + my $url = new URI::URL ( $url_string ); my(@path)=$url->path_components; splice @path, 0-$up; $url->path_components(@path); @@ -193,6 +232,9 @@ Returns HTML tag for beginning a table. =cut sub table { + use Carp; + carp 'FS::CGI::table deprecated; include /elements/table.html instead'; + my $col = shift; if ( $col ) { qq!<TABLE BGCOLOR="$col" BORDER=1 WIDTH="100%" CELLSPACING=0 CELLPADDING=2 BORDERCOLOR="#999999">!; @@ -290,6 +332,10 @@ sub small_custview { $html .= '</TR></TABLE>'; + $html .= '<BR>Balance: <B>$'. $cust_main->balance. '</B><BR>'; + + # last payment might be good here too? + $html; } diff --git a/FS/FS/ClientAPI.pm b/FS/FS/ClientAPI.pm index f7b8eb028..7cbbdbf67 100644 --- a/FS/FS/ClientAPI.pm +++ b/FS/FS/ClientAPI.pm @@ -1,13 +1,13 @@ package FS::ClientAPI; use strict; -use vars qw(%handler); +use vars qw(%handler $domain); %handler = (); #find modules foreach my $INC ( @INC ) { - foreach my $file ( glob("$INC/FS/ClientAPI/*") ) { + foreach my $file ( glob("$INC/FS/ClientAPI/*.pm") ) { $file =~ /\/(\w+)\.pm$/ or do { warn "unrecognized ClientAPI file: $file"; next diff --git a/FS/FS/ClientAPI/MyAccount.pm b/FS/FS/ClientAPI/MyAccount.pm index 674785524..445f0ece8 100644 --- a/FS/FS/ClientAPI/MyAccount.pm +++ b/FS/FS/ClientAPI/MyAccount.pm @@ -4,24 +4,45 @@ use strict; use vars qw($cache); use Digest::MD5 qw(md5_hex); use Date::Format; +use Business::CreditCard; use Cache::SharedMemoryCache; #store in db? use FS::CGI qw(small_custview); #doh use FS::Conf; -use FS::Record qw(qsearchs); +use FS::Record qw(qsearch qsearchs); use FS::svc_acct; use FS::svc_domain; use FS::cust_main; use FS::cust_bill; +use FS::cust_main_county; +use FS::cust_pkg; use FS::ClientAPI; #hmm FS::ClientAPI->register_handlers( - 'MyAccount/login' => \&login, - 'MyAccount/customer_info' => \&customer_info, - 'MyAccount/invoice' => \&invoice, + 'MyAccount/login' => \&login, + 'MyAccount/customer_info' => \&customer_info, + 'MyAccount/edit_info' => \&edit_info, + 'MyAccount/invoice' => \&invoice, + 'MyAccount/cancel' => \&cancel, + 'MyAccount/payment_info' => \&payment_info, + 'MyAccount/process_payment' => \&process_payment, + 'MyAccount/list_pkgs' => \&list_pkgs, + 'MyAccount/order_pkg' => \&order_pkg, + 'MyAccount/cancel_pkg' => \&cancel_pkg, + 'MyAccount/charge' => \&charge, +); + +use vars qw( @cust_main_editable_fields ); +@cust_main_editable_fields = qw( + first last company address1 address2 city + county state zip country daytime night fax + ship_first ship_last ship_company ship_address1 ship_address2 ship_city + ship_state ship_zip ship_country ship_daytime ship_night ship_fax ); #store in db? -my $cache = new Cache::SharedMemoryCache(); +my $cache = new Cache::SharedMemoryCache( { + 'namespace' => 'FS::ClientAPI::MyAccount', +} ); #false laziness w/FS::ClientAPI::passwd::passwd (needs to handle encrypted pw) sub login { @@ -95,6 +116,10 @@ sub customer_info { $return{name} = $cust_main->first. ' '. $cust_main->get('last'); + for (@cust_main_editable_fields) { + $return{$_} = $cust_main->get($_); + } + } else { #no customer record my $svc_acct = qsearchs('svc_acct', { 'svcnum' => $session->{'svcnum'} } ) @@ -103,7 +128,6 @@ sub customer_info { } - return { 'error' => '', 'custnum' => $custnum, %return, @@ -111,6 +135,125 @@ sub customer_info { } +sub edit_info { + my $p = shift; + my $session = $cache->get($p->{'session_id'}) + or return { 'error' => "Can't resume session" }; #better error message + + my $custnum = $session->{'custnum'} + or return { 'error' => "no customer record" }; + + my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } ) + or return { 'error' => "unknown custnum $custnum" }; + + my $new = new FS::cust_main { $cust_main->hash }; + $new->set( $_ => $p->{$_} ) + foreach grep { exists $p->{$_} } @cust_main_editable_fields; + my $error = $new->replace($cust_main); + return { 'error' => $error } if $error; + #$cust_main = $new; + + return { 'error' => '' }; +} + +sub payment_info { + my $p = shift; + my $session = $cache->get($p->{'session_id'}) + or return { 'error' => "Can't resume session" }; #better error message + + my %return; + + my $custnum = $session->{'custnum'}; + + my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } ) + or return { 'error' => "unknown custnum $custnum" }; + + $return{balance} = $cust_main->balance; + + $return{payname} = $cust_main->payname + || ( $cust_main->first. ' '. $cust_main->get('last') ); + + $return{$_} = $cust_main->get($_) for qw(address1 address2 city state zip); + + $return{payby} = $cust_main->payby; + + if ( $cust_main->payby =~ /^(CARD|DCRD)$/ ) { + warn $return{card_type} = cardtype($cust_main->payinfo); + $return{payinfo} = $cust_main->payinfo; + + if ( $cust_main->paydate =~ /^(\d{4})-(\d{2})-\d{2}$/ ) { #Pg date format + @return{'month', 'year'} = ( $2, $1 ); + } elsif ( $cust_main->paydate =~ /^(\d{1,2})-(\d{1,2}-)?(\d{4}$)/ ) { + @return{'month', 'year'} = ( $1, $3 ); + } + + } + + #list all counties/states/countries + $return{'cust_main_county'} = + [ map { $_->hashref } qsearch('cust_main_county', {}) ], + + #shortcut for one-country folks + my $conf = new FS::Conf; + my %states = map { $_->state => 1 } + qsearch('cust_main_county', { + 'country' => $conf->config('defaultcountry') || 'US' + } ); + $return{'states'} = [ sort { $a cmp $b } keys %states ]; + + $return{card_types} = { + 'VISA' => 'VISA card', + 'MasterCard' => 'MasterCard', + 'Discover' => 'Discover card', + 'American Express' => 'American Express card', + }; + + my $_date = time; + $return{paybatch} = "webui-MyAccount-$_date-$$-". rand() * 2**32; + + return { 'error' => '', + %return, + }; + +}; + +sub process_payment { + my $p = shift; + + my $session = $cache->get($p->{'session_id'}) + or return { 'error' => "Can't resume session" }; #better error message + + my %return; + + my $custnum = $session->{'custnum'}; + + my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } ) + or return { 'error' => "unknown custnum $custnum" }; + + if ( $p->{'save'} ) { + my $new = new FS::cust_main { $cust_main->hash }; + $new->set( $_ => $p->{$_} ) + foreach qw( payname address1 address2 city state zip payinfo ); + $new->set( 'paydate' => $p->{'year'}. '-'. $p->{'month'}. '-01' ); + $new->set( 'payby' => $p->{'auto'} ? 'CARD' : 'DCRD' ); + my $error = $new->replace($cust_main); + return { 'error' => $error } if $error; + $cust_main = $new; + } + + my $error = $cust_main->realtime_bop( 'CC', $p->{'amount'}, quiet=>1, + 'paydate' => $p->{'year'}. '-'. $p->{'month'}. '-01', + map { $_ => $p->{$_} } + qw( payname address1 address2 city state zip payinfo paybatch ) + ); + return { 'error' => $error } if $error; + + $cust_main->apply_payments; + + return { 'error' => '' }; + +} + sub invoice { my $p = shift; my $session = $cache->get($p->{'session_id'}) @@ -133,4 +276,135 @@ sub invoice { } +sub cancel { + my $p = shift; + my $session = $cache->get($p->{'session_id'}) + or return { 'error' => "Can't resume session" }; #better error message + + my $custnum = $session->{'custnum'}; + + my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } ) + or return { 'error' => "unknown custnum $custnum" }; + + my @errors = $cust_main->cancel( 'quiet'=>1 ); + + my $error = scalar(@errors) ? join(' / ', @errors) : ''; + + return { 'error' => $error }; + +} + +sub list_pkgs { + my $p = shift; + my $session = $cache->get($p->{'session_id'}) + or return { 'error' => "Can't resume session" }; #better error message + + my $custnum = $session->{'custnum'}; + + my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } ) + or return { 'error' => "unknown custnum $custnum" }; + + return { 'cust_pkg' => [ map { $_->hashref } $cust_main->ncancelled_pkgs ] }; + +} + +sub order_pkg { + my $p = shift; + my $session = $cache->get($p->{'session_id'}) + or return { 'error' => "Can't resume session" }; #better error message + + my $custnum = $session->{'custnum'}; + + my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } ) + or return { 'error' => "unknown custnum $custnum" }; + + #false laziness w/ClientAPI/Signup.pm + + my $cust_pkg = new FS::cust_pkg ( { + 'custnum' => $custnum, + 'pkgpart' => $p->{'pkgpart'}, + } ); + my $error = $cust_pkg->check; + return { 'error' => $error } if $error; + + my $svc_acct = new FS::svc_acct ( { + 'svcpart' => $p->{'svcpart'} || $cust_pkg->part_pkg->svcpart('svc_acct'), + map { $_ => $p->{$_} } + qw( username _password sec_phrase popnum ), + } ); + + my @acct_snarf; + my $snarfnum = 1; + while ( length($p->{"snarf_machine$snarfnum"}) ) { + my $acct_snarf = new FS::acct_snarf ( { + 'machine' => $p->{"snarf_machine$snarfnum"}, + 'protocol' => $p->{"snarf_protocol$snarfnum"}, + 'username' => $p->{"snarf_username$snarfnum"}, + '_password' => $p->{"snarf_password$snarfnum"}, + } ); + $snarfnum++; + push @acct_snarf, $acct_snarf; + } + $svc_acct->child_objects( \@acct_snarf ); + + my $y = $svc_acct->setdefault; # arguably should be in new method + return { 'error' => $y } if $y && !ref($y); + + $error = $svc_acct->check; + return { 'error' => $error } if $error; + + use Tie::RefHash; + tie my %hash, 'Tie::RefHash'; + %hash = ( $cust_pkg => [ $svc_acct ] ); + #msgcat + $error = $cust_main->order_pkgs( \%hash, '', 'noexport' => 1 ); + return { 'error' => $error } if $error; + + my $conf = new FS::Conf; + if ( $conf->exists('signup_server-realtime') ) { + + my $old_balance = $cust_main->balance; + + my $bill_error = $cust_main->bill; + $cust_main->apply_payments; + $cust_main->apply_credits; + $bill_error = $cust_main->collect; + + if ( $cust_main->balance > $old_balance ) { + $cust_pkg->cancel('quiet'=>1); + return { 'error' => '_decline' }; + } else { + $cust_pkg->reexport; + } + + } else { + $cust_pkg->reexport; + } + + return { error => '' }; + +} + +sub cancel_pkg { + my $p = shift; + my $session = $cache->get($p->{'session_id'}) + or return { 'error' => "Can't resume session" }; #better error message + + my $custnum = $session->{'custnum'}; + + my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } ) + or return { 'error' => "unknown custnum $custnum" }; + + my $pkgnum = $session->{'pkgnum'}; + + my $cust_pkg = qsearchs('cust_pkg', { 'custnum' => $custnum, + 'pkgnum' => $pkgnum, } ) + or return { 'error' => "unknown pkgnum $pkgnum" }; + + my $error = $cust_main->cancel( 'quiet'=>1 ); + return { 'error' => $error }; + +} + +1; diff --git a/FS/FS/ClientAPI/Signup.pm b/FS/FS/ClientAPI/Signup.pm new file mode 100644 index 000000000..375958b9c --- /dev/null +++ b/FS/FS/ClientAPI/Signup.pm @@ -0,0 +1,235 @@ +package FS::ClientAPI::Signup; + +use strict; +use Tie::RefHash; +use FS::Conf; +use FS::Record qw(qsearch qsearchs dbdef); +use FS::agent; +use FS::cust_main_county; +use FS::part_pkg; +use FS::svc_acct_pop; +use FS::cust_main; +use FS::cust_pkg; +use FS::svc_acct; +use FS::acct_snarf; +use FS::Msgcat qw(gettext); + +use FS::ClientAPI; #hmm +FS::ClientAPI->register_handlers( + 'Signup/signup_info' => \&signup_info, + 'Signup/new_customer' => \&new_customer, +); + +sub signup_info { + #my $packet = shift; + + my $conf = new FS::Conf; + + use vars qw($signup_info); #cache for performance; + $signup_info ||= { + + 'cust_main_county' => + [ map { $_->hashref } qsearch('cust_main_county', {}) ], + + 'agent' => + [ + map { $_->hashref } + qsearch('agent', dbdef->table('agent')->column('disabled') + ? { 'disabled' => '' } + : {} + ) + ], + + 'part_referral' => + [ + map { $_->hashref } + qsearch('part_referral', + dbdef->table('part_referral')->column('disabled') + ? { 'disabled' => '' } + : {} + ) + ], + + 'agentnum2part_pkg' => + { + map { + my $href = $_->pkgpart_hashref; + $_->agentnum => + [ + map { { 'payby' => [ $_->payby ], %{$_->hashref} } } + grep { $_->svcpart('svc_acct') && $href->{ $_->pkgpart } } + qsearch( 'part_pkg', { 'disabled' => '' } ) + ]; + } qsearch('agent', dbdef->table('agent')->column('disabled') + ? { 'disabled' => '' } + : {} + ) + }, + + 'svc_acct_pop' => [ map { $_->hashref } qsearch('svc_acct_pop',{} ) ], + + 'security_phrase' => $conf->exists('security_phrase'), + + 'payby' => [ $conf->config('signup_server-payby') ], + + 'cvv_enabled' => defined dbdef->table('cust_main')->column('paycvv'), + + 'msgcat' => { map { $_=>gettext($_) } qw( + passwords_dont_match invalid_card unknown_card_type not_a + ) }, + + 'statedefault' => $conf->config('statedefault') || 'CA', + + 'countrydefault' => $conf->config('countrydefault') || 'US', + + 'refnum' => $conf->config('signup_server-default_refnum'), + + }; + + if ( + $conf->config('signup_server-default_agentnum') + && !exists $signup_info->{'part_pkg'} #cache for performance + ) { + my $agentnum = $conf->config('signup_server-default_agentnum'); + my $agent = qsearchs( 'agent', { 'agentnum' => $agentnum } ) + or die "fatal: signup_server-default_agentnum $agentnum not found\n"; + my $pkgpart_href = $agent->pkgpart_hashref; + + $signup_info->{'part_pkg'} = [ + #map { $_->hashref } + map { { 'payby' => [ $_->payby ], %{$_->hashref} } } + grep { $_->svcpart('svc_acct') && $pkgpart_href->{ $_->pkgpart } } + qsearch( 'part_pkg', { 'disabled' => '' } ) + ]; + } + + $signup_info; + +} + +sub new_customer { + my $packet = shift; + + my $conf = new FS::Conf; + + #things that aren't necessary in base class, but are for signup server + #return "Passwords don't match" + # if $hashref->{'_password'} ne $hashref->{'_password2'} + return { 'error' => gettext('empty_password') } + unless $packet->{'_password'}; + # a bit inefficient for large numbers of pops + return { 'error' => gettext('no_access_number_selected') } + unless $packet->{'popnum'} || !scalar(qsearch('svc_acct_pop',{} )); + + #shares some stuff with htdocs/edit/process/cust_main.cgi... take any + # common that are still here and library them. + my $cust_main = new FS::cust_main ( { + #'custnum' => '', + 'agentnum' => $packet->{agentnum} + || $conf->config('signup_server-default_agentnum'), + 'refnum' => $packet->{refnum} + || $conf->config('signup_server-default_refnum'), + + map { $_ => $packet->{$_} } qw( + last first ss company address1 address2 city county state zip country + daytime night fax payby payinfo paycvv paydate payname referral_custnum + comments + ), + + } ); + + return { 'error' => "Illegal payment type" } + unless grep { $_ eq $packet->{'payby'} } + $conf->config('signup_server-payby'); + + $cust_main->payinfo($cust_main->daytime) + if $cust_main->payby eq 'LECB' && ! $cust_main->payinfo; + + my @invoicing_list = split( /\s*\,\s*/, $packet->{'invoicing_list'} ); + + $packet->{'pkgpart'} =~ /^(\d+)$/ or '' =~ /^()$/; + my $pkgpart = $1; + return { 'error' => 'Please select a package' } unless $pkgpart; #msgcat + + my $part_pkg = + qsearchs( 'part_pkg', { 'pkgpart' => $pkgpart } ) + or return { 'error' => "WARNING: unknown pkgpart: $pkgpart" }; + my $svcpart = $part_pkg->svcpart('svc_acct'); + + my $cust_pkg = new FS::cust_pkg ( { + #later#'custnum' => $custnum, + 'pkgpart' => $packet->{'pkgpart'}, + } ); + my $error = $cust_pkg->check; + return { 'error' => $error } if $error; + + my $svc_acct = new FS::svc_acct ( { + 'svcpart' => $svcpart, + map { $_ => $packet->{$_} } + qw( username _password sec_phrase popnum ), + } ); + + my @acct_snarf; + my $snarfnum = 1; + while ( length($packet->{"snarf_machine$snarfnum"}) ) { + my $acct_snarf = new FS::acct_snarf ( { + 'machine' => $packet->{"snarf_machine$snarfnum"}, + 'protocol' => $packet->{"snarf_protocol$snarfnum"}, + 'username' => $packet->{"snarf_username$snarfnum"}, + '_password' => $packet->{"snarf_password$snarfnum"}, + } ); + $snarfnum++; + push @acct_snarf, $acct_snarf; + } + $svc_acct->child_objects( \@acct_snarf ); + + my $y = $svc_acct->setdefault; # arguably should be in new method + return { 'error' => $y } if $y && !ref($y); + + $error = $svc_acct->check; + return { 'error' => $error } if $error; + + use Tie::RefHash; + tie my %hash, 'Tie::RefHash'; + %hash = ( $cust_pkg => [ $svc_acct ] ); + #msgcat + $error = $cust_main->insert( \%hash, \@invoicing_list, 'noexport' => 1 ); + return { 'error' => $error } if $error; + + if ( $conf->exists('signup_server-realtime') ) { + + #warn "[fs_signup_server] Billing customer...\n" if $Debug; + + my $bill_error = $cust_main->bill; + #warn "[fs_signup_server] error billing new customer: $bill_error" + # if $bill_error; + + $cust_main->apply_payments; + $cust_main->apply_credits; + + $bill_error = $cust_main->collect; + #warn "[fs_signup_server] error collecting from new customer: $bill_error" + # if $bill_error; + + if ( $cust_main->balance > 0 ) { + + #this makes sense. credit is "un-doing" the invoice + $cust_main->credit( $cust_main->balance, 'signup server decline' ); + $cust_main->apply_credits; + + #should check list for errors... + #$cust_main->suspend; + local $FS::svc_Common::noexport_hack = 1; + $cust_main->cancel('quiet'=>1); + + return { 'error' => '_decline' }; + } + + } + $cust_main->reexport; + + return { error => '' }; + +} + +1; diff --git a/FS/FS/ClientAPI/passwd.pm b/FS/FS/ClientAPI/passwd.pm index 29606227d..016ebff79 100644 --- a/FS/FS/ClientAPI/passwd.pm +++ b/FS/FS/ClientAPI/passwd.pm @@ -15,8 +15,9 @@ FS::ClientAPI->register_handlers( sub passwd { my $packet = shift; - #my $domain = qsearchs('svc_domain', { 'domain' => $packet->{'domain'} } ) - # or return { error => "Domain $domain not found" }; + my $domain = $FS::ClientAPI::domain || $packet->{'domain'}; + my $svc_domain = qsearchs('svc_domain', { 'domain' => $domain } ) + or return { error => "Domain $domain not found" }; my $old_password = $packet->{'old_password'}; my $new_password = $packet->{'new_password'}; @@ -27,11 +28,11 @@ sub passwd { my $svc_acct = ( length($old_password) < 13 && qsearchs( 'svc_acct', { 'username' => $packet->{'username'}, - #'domsvc' => $svc_domain->svcnum, + 'domsvc' => $svc_domain->svcnum, '_password' => $old_password } ) ) || qsearchs( 'svc_acct', { 'username' => $packet->{'username'}, - #'domsvc' => $svc_domain->svcnum, + 'domsvc' => $svc_domain->svcnum, '_password' => $old_password } ); unless ( $svc_acct ) { return { error => 'Incorrect password.' } } diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm index e93eaf3fc..99eee18ea 100644 --- a/FS/FS/Conf.pm +++ b/FS/FS/Conf.pm @@ -174,7 +174,7 @@ sub config_items { my $self = shift; #quelle kludge @config_items, - map { + ( map { my $basename = basename($_); $basename =~ /^(.*)$/; $basename = $1; @@ -185,7 +185,19 @@ sub config_items { 'type' => 'textarea', } } glob($self->dir. '/invoice_template_*') - ; + ), + ( map { + my $basename = basename($_); + $basename =~ /^(.*)$/; + $basename = $1; + new FS::ConfItem { + 'key' => $basename, + 'section' => 'billing', + 'description' => 'Alternate LaTeX template for invoices. See the <a href="../docs/billing.html">billing documentation</a> for details.', + 'type' => 'textarea', + } + } glob($self->dir. '/invoice_latex_*') + ); } =back @@ -228,8 +240,8 @@ httemplate/docs/config.html { 'key' => 'apacheip', - 'section' => 'apache', - 'description' => 'The current IP address to assign to new virtual hosts', + 'section' => 'deprecated', + 'description' => '<b>DEPRECATED</b>, add an <i>apache</i> <a href="../browse/part_export.cgi">export</a> instead. Used to be the current IP address to assign to new virtual hosts', 'type' => 'text', }, @@ -242,8 +254,8 @@ httemplate/docs/config.html { 'key' => 'apachemachines', - 'section' => 'apache', - 'description' => 'Your Apache machines, one per line. This enables export of `/etc/apache/vhosts.conf\', which can be included in your Apache configuration via the <a href="http://www.apache.org/docs/mod/core.html#include">Include</a> directive.', + 'section' => 'deprecated', + 'description' => '<b>DEPRECATED</b>, add an <i>apache</i> <a href="../browse/part_export.cgi">export</a> instead. Used to be Apache machines, one per line. This enables export of `/etc/apache/vhosts.conf\', which can be included in your Apache configuration via the <a href="http://www.apache.org/docs/mod/core.html#include">Include</a> directive.', 'type' => 'textarea', }, @@ -269,9 +281,16 @@ httemplate/docs/config.html }, { + 'key' => 'business-onlinepayment-ach', + 'section' => 'billing', + 'description' => 'Alternate <a href="http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment">Business::OnlinePayment</a> support for ACH transactions (defaults to regular <b>business-onlinepayment</b>). At least three lines: processor, login, and password. An optional fourth line specifies the action or actions (multiple actions are separated with `,\': for example: `Authorization Only, Post Authorization\'). Optional additional lines are passed to Business::OnlinePayment as %processor_options.', + 'type' => 'textarea', + }, + + { 'key' => 'business-onlinepayment-description', 'section' => 'billing', - 'description' => 'String passed as the description field to <a href="http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment">Business::OnlinePayment</a>. Evaluated as a double-quoted perl string, with the following variables available: <code>$agent</code> (the agent name), and <code>$pkgs</code> (a comma-separated list of packages to which the invoiced being charged applies)', + 'description' => 'String passed as the description field to <a href="http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment">Business::OnlinePayment</a>. Evaluated as a double-quoted perl string, with the following variables available: <code>$agent</code> (the agent name), and <code>$pkgs</code> (a comma-separated list of packages for which these charges apply)', 'type' => 'text', }, @@ -290,13 +309,6 @@ httemplate/docs/config.html }, { - 'key' => 'cybercash3.2', - 'section' => 'billing', - 'description' => '<a href="http://www.cybercash.com/cashregister/">CyberCash Cashregister v3.2</a> support. Two lines: the full path and name of your merchant_conf file, and the transaction type (`mauthonly\' or `mauthcapture\').', - 'type' => 'textarea', - }, - - { 'key' => 'cyrus', 'section' => 'deprecated', 'description' => '<b>DEPRECATED</b>, add a <i>cyrus</i> <a href="../browse/part_export.cgi">export</a> instead. This option used to integrate with <a href="http://asg.web.cmu.edu/cyrus/imapd/">Cyrus IMAP Server</a>, three lines: IMAP server, admin username, and admin password. Cyrus::IMAP::Admin should be installed locally and the connection to the server secured.', @@ -320,11 +332,25 @@ httemplate/docs/config.html { 'key' => 'deletepayments', 'section' => 'UI', - 'description' => 'Enable deletion of unclosed payments. Be very careful! Only delete payments that were data-entry errors, not adjustments. Optionally specify one or more comma-separated email addresses to be notified when a payment is deleted.', + 'description' => 'Enable deletion of unclosed payments. Be very careful! Only delete payments that were data-entry errors, not adjustments. Optionally specify one or more comma-separated email addresses to be notified when a payment is deleted.', + 'type' => [qw( checkbox text )], + }, + + { + 'key' => 'deletecredits', + 'section' => 'UI', + 'description' => 'Enable deletion of unclosed credits. Be very careful! Only delete credits that were data-entry errors, not adjustments. Optionally specify one or more comma-separated email addresses to be notified when a credit is deleted.', 'type' => [qw( checkbox text )], }, { + 'key' => 'unapplypayments', + 'section' => 'UI', + 'description' => 'Enable "unapplication" of unclosed payments.', + 'type' => 'checkbox', + }, + + { 'key' => 'dirhash', 'section' => 'shell', 'description' => 'Optional numeric value to control directory hashing. If positive, hashes directories for the specified number of levels from the front of the username. If negative, hashes directories for the specified number of levels from the end of the username. Some examples: <ul><li>1: user -> <a href="#home">/home</a>/u/user<li>2: user -> <a href="#home">/home</a>/u/s/user<li>-1: user -> <a href="#home">/home</a>/r/user<li>-2: user -> <a href="#home">home</a>/r/e/user</ul>', @@ -339,13 +365,6 @@ httemplate/docs/config.html }, { - 'key' => 'domain', - 'section' => 'deprecated', - 'description' => 'Your domain name.', - 'type' => 'text', - }, - - { 'key' => 'editreferrals', 'section' => 'UI', 'description' => 'Enable advertising source modification for existing customers', @@ -369,14 +388,21 @@ httemplate/docs/config.html { 'key' => 'emailinvoiceauto', 'section' => 'billing', - 'description' => 'Automatically adds new accounts to the email invoice list upon customer creation', + 'description' => 'Automatically adds new accounts to the email invoice list', 'type' => 'checkbox', }, { - 'key' => 'erpcdmachines', + 'key' => 'exclude_ip_addr', 'section' => '', - 'description' => 'Your ERPCD authenticaion machines, one per line. This enables export of `/usr/annex/acp_passwd\' and `/usr/annex/acp_dialup\'', + 'description' => 'Exclude these from the list of available broadband service IP addresses. (One per line)', + 'type' => 'textarea', + }, + + { + 'key' => 'erpcdmachines', + 'section' => 'deprecated', + 'description' => '<b>DEPRECATED</b>, ERPCD is no longer supported. Used to be ERPCD authenticaion machines, one per line. This enables export of `/usr/annex/acp_passwd\' and `/usr/annex/acp_dialup\'', 'type' => 'textarea', }, @@ -411,21 +437,21 @@ httemplate/docs/config.html { 'key' => 'icradius_mysqldest', 'section' => 'deprecated', - 'description' => '<b>DEPRECATED</b>, add an <i>sqlradius</i> https://billing.crosswind.net/freeside/browse/part_export.cgi">export</a> instead. Used to be the destination directory for the MySQL databases, on the ICRADIUS/FreeRADIUS machines. Defaults to "/usr/local/var/".', + 'description' => '<b>DEPRECATED</b>, add an <i>sqlradius</i> <a href="../browse/part_export.cgi">export</a> instead. Used to be the destination directory for the MySQL databases, on the ICRADIUS/FreeRADIUS machines. Defaults to "/usr/local/var/".', 'type' => 'text', }, { 'key' => 'icradius_mysqlsource', 'section' => 'deprecated', - 'description' => '<b>DEPRECATED</b>, add an <i>sqlradius</i> https://billing.crosswind.net/freeside/browse/part_export.cgi">export</a> instead. Used to be the source directory for for the MySQL radcheck table files, on the Freeside machine. Defaults to "/usr/local/var/freeside".', + 'description' => '<b>DEPRECATED</b>, add an <i>sqlradius</i> <a href="../browse/part_export.cgi">export</a> instead. Used to be the source directory for for the MySQL radcheck table files, on the Freeside machine. Defaults to "/usr/local/var/freeside".', 'type' => 'text', }, { 'key' => 'icradius_secrets', 'section' => 'deprecated', - 'description' => '<b>DEPRECATED</b>, add an <i>sqlradius</i> https://billing.crosswind.net/freeside/browse/part_export.cgi">export</a> instead. This option used to specify a database for ICRADIUS/FreeRADIUS export. Three lines: DBI data source, username and password.', + 'description' => '<b>DEPRECATED</b>, add an <i>sqlradius</i> <a href="../browse/part_export.cgi">export</a> instead. This option used to specify a database for ICRADIUS/FreeRADIUS export. Three lines: DBI data source, username and password.', 'type' => 'textarea', }, @@ -444,6 +470,49 @@ httemplate/docs/config.html }, { + 'key' => 'invoice_latex', + 'section' => 'billing', + 'description' => 'Optional LaTeX template for typeset PostScript invoices.', + 'type' => 'textarea', + }, + + { + 'key' => 'invoice_latexnotes', + 'section' => 'billing', + 'description' => 'Notes section for LaTeX typeset PostScript invoices.', + 'type' => 'textarea', + }, + + { + 'key' => 'invoice_latexfooter', + 'section' => 'billing', + 'description' => 'Footer for LaTeX typeset PostScript invoices.', + 'type' => 'textarea', + }, + + { + 'key' => 'invoice_latexsmallfooter', + 'section' => 'billing', + 'description' => 'Optional small footer for multi-page LaTeX typeset PostScript invoices.', + 'type' => 'textarea', + }, + + { + 'key' => 'invoice_default_terms', + 'section' => 'billing', + 'description' => 'Optional default invoice term, used to calculate a due date printed on invoices.', + 'type' => 'select', + 'select_enum' => [ '', 'Payable upon receipt', 'Net 0', 'Net 10', 'Net 15', 'Net 30', 'Net 45', 'Net 60' ], + }, + + { + 'key' => 'invoice_send_receipts', + 'section' => 'billing', + 'description' => 'Send receipts for payments and credits.', + 'type' => 'checkbox', + }, + + { 'key' => 'lpr', 'section' => 'required', 'description' => 'Print command for paper invoices, for example `lpr -h\'', @@ -527,8 +596,8 @@ httemplate/docs/config.html { 'key' => 'qmailmachines', - 'section' => 'mail', - 'description' => 'Your qmail machines, one per line. This enables export of `/var/qmail/control/virtualdomains\', `/var/qmail/control/recipientmap\', and `/var/qmail/control/rcpthosts\'. Setting this option (even if empty) also turns on user `.qmail-extension\' file maintenance in conjunction with the <b>shellmachine</b> option.', + 'section' => 'deprecated', + 'description' => '<b>DEPRECATED</b>, add <i>qmail</i> and <i>shellcommands</i> <a href="../browse/part_export.cgi">exports</a> instead. This option used to export `/var/qmail/control/virtualdomains\', `/var/qmail/control/recipientmap\', and `/var/qmail/control/rcpthosts\'. Setting this option (even if empty) also turns on user `.qmail-extension\' file maintenance in conjunction with the <b>shellmachine</b> option.', 'type' => [qw( checkbox textarea )], }, @@ -569,22 +638,22 @@ httemplate/docs/config.html { 'key' => 'sendmailconfigpath', - 'section' => 'mail', - 'description' => 'Sendmail configuration file path. Defaults to `/etc\'. Many newer distributions use `/etc/mail\'.', + 'section' => 'deprecated', + 'description' => '<b>DEPRECATED</b>, add a <i>sendmail</i> <a href="../browse/part_export.cgi">export</a> instead. Used to be sendmail configuration file path. Defaults to `/etc\'. Many newer distributions use `/etc/mail\'.', 'type' => 'text', }, { 'key' => 'sendmailmachines', - 'section' => 'mail', - 'description' => 'Your sendmail machines, one per line. This enables export of `/etc/virtusertable\' and `/etc/sendmail.cw\'.', + 'section' => 'deprecated', + 'description' => '<b>DEPRECATED</b>, add a <i>sendmail</i> <a href="../browse/part_export.cgi">export</a> instead. Used to be sendmail machines, one per line. This enables export of `/etc/virtusertable\' and `/etc/sendmail.cw\'.', 'type' => 'textarea', }, { 'key' => 'sendmailrestart', - 'section' => 'mail', - 'description' => 'If defined, the command which is run on sendmail machines after files are copied.', + 'section' => 'deprecated', + 'description' => '<b>DEPRECATED</b>, add a <i>sendmail</i> <a href="../browse/part_export.cgi">export</a> instead. Used to define the command which is run on sendmail machines after files are copied.', 'type' => 'text', }, @@ -753,7 +822,7 @@ httemplate/docs/config.html { 'key' => 'username-ampersand', 'section' => 'username', - 'description' => 'Allow the ampersand character (&) in usernames. Be careful when using this option in conjunction with <a href="#shellmachine-useradd">shellmachine-useradd</a> and other configuration options which execute shell commands, as the ampersand will be interpreted by the shell if not quoted.', + 'description' => 'Allow the ampersand character (&) in usernames. Be careful when using this option in conjunction with <a href="../browse/part_export.cgi">exports</a> which execute shell commands, as the ampersand will be interpreted by the shell if not quoted.', 'type' => 'checkbox', }, @@ -801,7 +870,7 @@ httemplate/docs/config.html { 'key' => 'username_policy', - 'section' => '', + 'section' => 'deprecated', 'description' => 'This file controls the mechanism for preventing duplicate usernames in passwd/radius files exported from svc_accts. This should be one of \'prepend domsvc\' \'append domsvc\' \'append domain\' or \'append @domain\'', 'type' => 'select', 'select_enum' => [ 'prepend domsvc', 'append domsvc', 'append domain', 'append @domain' ], @@ -811,14 +880,14 @@ httemplate/docs/config.html { 'key' => 'vpopmailmachines', 'section' => 'deprecated', - 'description' => '<b>DEPRECATED</b>, add a <i>cp</i> <a href="../browse/part_export.cgi">export</a> instead. This option used to contain your vpopmail pop toasters, one per line. Each line is of the form "machinename vpopdir vpopuid vpopgid". For example: <code>poptoaster.domain.tld /home/vpopmail 508 508</code> Note: vpopuid and vpopgid are values taken from the vpopmail machine\'s /etc/passwd', + 'description' => '<b>DEPRECATED</b>, add a <i>vpopmail</i> <a href="../browse/part_export.cgi">export</a> instead. This option used to contain your vpopmail pop toasters, one per line. Each line is of the form "machinename vpopdir vpopuid vpopgid". For example: <code>poptoaster.domain.tld /home/vpopmail 508 508</code> Note: vpopuid and vpopgid are values taken from the vpopmail machine\'s /etc/passwd', 'type' => 'textarea', }, { 'key' => 'vpopmailrestart', - 'section' => 'mail', - 'description' => 'If defined, the shell commands to run on vpopmail machines after files are copied. An example can be found in eg/vpopmailrestart of the source distribution.', + 'section' => 'deprecated', + 'description' => '<b>DEPRECATED</b>, add a <i>vpopmail</i> <a href="../browse/part_export.cgi">export</a> instead. This option used to define the shell commands to run on vpopmail machines after files are copied. An example can be found in eg/vpopmailrestart of the source distribution.', 'type' => 'textarea', }, @@ -880,20 +949,47 @@ httemplate/docs/config.html }, { + 'key' => 'selfservice_server-quiet', + 'section' => 'deprecated', + 'description' => '<b>DEPRECATED</b>, the self-service server no longer sends superfluous decline and cancel emails. Used to disable decline and cancel emails generated by transactions initiated by the selfservice server.', + 'type' => 'checkbox', + }, + + { + 'key' => 'signup_server-quiet', + 'section' => 'deprecated', + 'description' => '<b>DEPRECATED</b>, the signup server is now part of the self-service server and no longer sends superfluous decline and cancel emails. Used to disable decline and cancel emails generated by transactions initiated by the signup server. Does not disable welcome emails.', + 'type' => 'checkbox', + }, + + { 'key' => 'signup_server-payby', 'section' => '', 'description' => 'Acceptable payment types for the signup server', 'type' => 'selectmultiple', - 'select_enum' => [ qw(CARD PREPAY BILL COMP) ], + 'select_enum' => [ qw(CARD DCRD CHEK DCHK LECB PREPAY BILL COMP) ], }, { 'key' => 'signup_server-email', + 'section' => 'deprecated', + 'description' => '<b>DEPRECATED</b>, this feature is no longer available. See the ***fill me in*** report instead. Used to contain a comma-separated list of email addresses to receive notification of signups via the signup server.', + 'type' => 'text', + }, + + { + 'key' => 'signup_server-default_agentnum', 'section' => '', - 'description' => 'Comma-separated list of email addresses to receive notification of signups via the signup server.', + 'description' => 'Default agentnum for the signup server', 'type' => 'text', }, + { + 'key' => 'signup_server-default_refnum', + 'section' => '', + 'description' => 'Default advertising source number for the signup server', + 'type' => 'text', + }, { 'key' => 'show-msgcat-codes', @@ -924,6 +1020,34 @@ httemplate/docs/config.html }, { + 'key' => 'emaildecline-exclude', + 'section' => 'billing', + 'description' => 'List of error messages that should not trigger email decline notices, one per line.', + 'type' => 'textarea', + }, + + { + 'key' => 'cancelmessage', + 'section' => 'billing', + 'description' => 'Template file for cancellation emails.', + 'type' => 'textarea', + }, + + { + 'key' => 'cancelsubject', + 'section' => 'billing', + 'description' => 'Subject line for cancellation emails.', + 'type' => 'text', + }, + + { + 'key' => 'emailcancel', + 'section' => 'billing', + 'description' => 'Enable emailing of cancellation notices.', + 'type' => 'checkbox', + }, + + { 'key' => 'require_cardname', 'section' => 'billing', 'description' => 'Require an "Exact name on card" to be entered explicitly; don\'t default to using the first and last name.', @@ -966,6 +1090,87 @@ httemplate/docs/config.html 'select_enum' => [ 'text/plain', 'text/html' ], }, + { + 'key' => 'payby-default', + 'section' => 'UI', + 'description' => 'Default payment type. HIDE disables display of billing information and sets customers to BILL.', + 'type' => 'select', + 'select_enum' => [ '', qw(CARD DCRD CHEK DCHK LECB BILL COMP HIDE) ], + }, + + { + 'key' => 'svc_acct-notes', + 'section' => 'UI', + 'description' => 'Extra HTML to be displayed on the Account View screen.', + 'type' => 'textarea', + }, + + { + 'key' => 'radius-password', + 'section' => '', + 'description' => 'RADIUS attribute for plain-text passwords.', + 'type' => 'select', + 'select_enum' => [ 'Password', 'User-Password' ], + }, + + { + 'key' => 'radius-ip', + 'section' => '', + 'description' => 'RADIUS attribute for IP addresses.', + 'type' => 'select', + 'select_enum' => [ 'Framed-IP-Address', 'Framed-Address' ], + }, + + { + 'key' => 'svc_acct-alldomains', + 'section' => '', + 'description' => 'Allow accounts to select any domain in the database. Normally accounts can only select from the domain set in the service definition and those purchased by the customer.', + 'type' => 'checkbox', + }, + + { + 'key' => 'dump-scpdest', + 'section' => '', + 'description' => 'destination for scp database dumps: user@host:/path', + 'type' => 'text', + }, + + { + 'key' => 'users-allow_comp', + 'section' => '', + 'description' => 'Usernames (Freeside users, created with <a href="../docs/man/bin/freeside-adduser.html">freeside-adduser</a>) which can create complimentary customers, one per line. If no usernames are entered, all users can create complimentary accounts.', + 'type' => 'textarea', + }, + + { + 'key' => 'cvv-save', + 'section' => 'billing', + 'description' => 'Save CVV2 information after the initial transaction for the selected credit card types. Enabling this option may be in violation of your merchant agreement(s), so please check them carefully before enabling this option for any credit card types.', + 'type' => 'selectmultiple', + 'select_enum' => [ "VISA card", + "MasterCard", + "Discover card", + "American Express card", + "Diner's Club/Carte Blanche", + "enRoute", + "JCB", + "BankCard", + ], + }, + + { + 'key' => 'allow_negative_charges', + 'section' => 'billing', + 'description' => 'Allow negative charges. Normally not used unless importing data from a legacy system that requires this.', + 'type' => 'checkbox', + }, + + { + 'key' => 'system_usernames', + 'section' => 'username', + 'description' => 'A list of system usernames that cannot be edited or removed, one per line. Use a bare username to prohibit modification/deletion of the username in any domain, or username@domain to prohibit modification/deletetion of a specific username and domain.', + 'type' => 'textarea', + }, ); 1; diff --git a/FS/FS/InitHandler.pm b/FS/FS/InitHandler.pm index 87f507c22..5038cf352 100644 --- a/FS/FS/InitHandler.pm +++ b/FS/FS/InitHandler.pm @@ -1,5 +1,9 @@ package FS::InitHandler; +# this leaks memory under graceful restarts and i wouldn't use it on any +# modern server. useful for very slow machines with memory to spare, just +# always do a full restart + use strict; use vars qw($DEBUG); use FS::UID qw(adminsuidsetup); @@ -48,7 +52,6 @@ sub handler { use FS::session; use FS::svc_acct; use FS::svc_acct_pop; - use FS::svc_acct_sm; use FS::svc_domain; use FS::svc_forward; use FS::svc_www; diff --git a/FS/FS/Misc.pm b/FS/FS/Misc.pm new file mode 100644 index 000000000..efad2dfd6 --- /dev/null +++ b/FS/FS/Misc.pm @@ -0,0 +1,102 @@ +package FS::Misc; + +use strict; +use vars qw ( @ISA @EXPORT_OK ); +use Exporter; + +@ISA = qw( Exporter ); +@EXPORT_OK = qw( send_email ); + +=head1 NAME + +FS::Misc - Miscellaneous subroutines + +=head1 SYNOPSIS + + use FS::Misc qw(send_email); + + send_email(); + +=head1 DESCRIPTION + +Miscellaneous subroutines. This module contains miscellaneous subroutines +called from multiple other modules. These are not OO or necessarily related, +but are collected here to elimiate code duplication. + +=head1 SUBROUTINES + +=over 4 + +=item send_email OPTION => VALUE ... + +Options: + +I<from> - (required) + +I<to> - (required) comma-separated scalar or arrayref of recipients + +I<subject> - (required) + +I<content-type> - (optional) MIME type + +I<body> - (required) arrayref of body text lines + +=cut + +use vars qw( $conf ); +use Date::Format; +use Mail::Header; +use Mail::Internet 1.44; +use FS::UID; + +FS::UID->install_callback( sub { + $conf = new FS::Conf; +} ); + +sub send_email { + my(%options) = @_; + + $ENV{MAILADDRESS} = $options{'from'}; + my $to = ref($options{to}) ? join(', ', @{ $options{to} } ) : $options{to}; + my @header = ( + 'From: '. $options{'from'}, + 'To: '. $to, + 'Sender: '. $options{'from'}, + 'Reply-To: '. $options{'from'}, + 'Date: '. time2str("%a, %d %b %Y %X %z", time), + 'Subject: '. $options{'subject'}, + ); + push @header, 'Content-Type: '. $options{'content-type'} + if exists($options{'content-type'}); + my $header = new Mail::Header ( \@header ); + + my $message = new Mail::Internet ( + 'Header' => $header, + 'Body' => $options{'body'}, + ); + + my $smtpmachine = $conf->config('smtpmachine'); + $!=0; + + my $rv = $message->smtpsend( 'Host' => $smtpmachine ) + or $message->smtpsend( Host => $smtpmachine, Debug => 1 ); + + if ($rv) { #smtpsend returns a list of addresses, not true/false + return ''; + } else { + return "can't send email to $to via server $smtpmachine with SMTP: $!"; + } + +} + +=head1 BUGS + +This package exists. + +=head1 SEE ALSO + +L<FS::UID>, L<FS::CGI>, L<FS::Record>, the base documentation. + +=cut + +1; diff --git a/FS/FS/Record.pm b/FS/FS/Record.pm index e6126a13b..801b89daf 100644 --- a/FS/FS/Record.pm +++ b/FS/FS/Record.pm @@ -2,18 +2,22 @@ package FS::Record; use strict; use vars qw( $dbdef_file $dbdef $setup_hack $AUTOLOAD @ISA @EXPORT_OK $DEBUG - $me %dbdef_cache ); + $me %dbdef_cache %virtual_fields_cache ); use subs qw(reload_dbdef); use Exporter; use Carp qw(carp cluck croak confess); use File::CounterFile; use Locale::Country; use DBI qw(:sql_types); -use DBIx::DBSchema 0.19; -use FS::UID qw(dbh checkruid getotaker datasrc driver_name); +use DBIx::DBSchema 0.23; +use FS::UID qw(dbh getotaker datasrc driver_name); use FS::SearchCache; use FS::Msgcat qw(gettext); +use FS::part_virtual_field; + +use Tie::IxHash; + @ISA = qw(Exporter); @EXPORT_OK = qw(dbh fields hfields qsearch qsearchs dbdef jsearch); @@ -60,14 +64,12 @@ FS::Record - Database record objects $hashref = $record->hashref; $error = $record->insert; - #$error = $record->add; #deprecated $error = $record->delete; - #$error = $record->del; #deprecated $error = $new_record->replace($old_record); - #$error = $new_record->rep($old_record); #deprecated + # external use deprecated - handled by the database (at least for Pg, mysql) $value = $record->unique('column'); $error = $record->ut_float('column'); @@ -88,7 +90,7 @@ FS::Record - Database record objects $quoted_value = _quote($value,'table','field'); - #depriciated + #deprecated $fields = hfields('table'); if ( $fields->{Field} ) { # etc. @@ -167,7 +169,7 @@ sub create { my $self = {}; bless ($self, $class); if ( defined $self->table ) { - cluck "create constructor is depriciated, use new!"; + cluck "create constructor is deprecated, use new!"; $self->new(@_); } else { croak "FS::Record::create called (not from a subclass)!"; @@ -202,45 +204,101 @@ sub qsearch { my $dbh = dbh; my $table = $cache ? $cache->table : $stable; + my $pkey = $dbdef->table($table)->primary_key; - my @fields = grep exists($record->{$_}), fields($table); + my @real_fields = grep exists($record->{$_}), real_fields($table); + my @virtual_fields = grep exists($record->{$_}), "FS::$table"->virtual_fields; my $statement = "SELECT $select FROM $stable"; - if ( @fields ) { - $statement .= ' WHERE '. join(' AND ', map { + if ( @real_fields or @virtual_fields ) { + $statement .= ' WHERE '. join(' AND ', + ( map { my $op = '='; + my $column = $_; if ( ref($record->{$_}) ) { $op = $record->{$_}{'op'} if $record->{$_}{'op'}; - $op = 'LIKE' if $op =~ /^ILIKE$/i && driver_name !~ /^Pg$/i; + #$op = 'LIKE' if $op =~ /^ILIKE$/i && driver_name ne 'Pg'; + if ( uc($op) eq 'ILIKE' ) { + $op = 'LIKE'; + $record->{$_}{'value'} = lc($record->{$_}{'value'}); + $column = "LOWER($_)"; + } $record->{$_} = $record->{$_}{'value'} } if ( ! defined( $record->{$_} ) || $record->{$_} eq '' ) { if ( $op eq '=' ) { - if ( driver_name =~ /^Pg$/i ) { - qq-( $_ IS NULL OR $_ = '' )-; + if ( driver_name eq 'Pg' ) { + my $type = $dbdef->table($table)->column($column)->type; + if ( $type =~ /(int|serial)/i ) { + qq-( $column IS NULL )-; + } else { + qq-( $column IS NULL OR $column = '' )-; + } } else { - qq-( $_ IS NULL OR $_ = "" )-; + qq-( $column IS NULL OR $column = "" )-; } } elsif ( $op eq '!=' ) { - if ( driver_name =~ /^Pg$/i ) { - qq-( $_ IS NOT NULL AND $_ != '' )-; + if ( driver_name eq 'Pg' ) { + my $type = $dbdef->table($table)->column($column)->type; + if ( $type =~ /(int|serial)/i ) { + qq-( $column IS NOT NULL )-; + } else { + qq-( $column IS NOT NULL AND $column != '' )-; + } } else { - qq-( $_ IS NOT NULL AND $_ != "" )-; + qq-( $column IS NOT NULL AND $column != "" )-; } } else { - if ( driver_name =~ /^Pg$/i ) { - qq-( $_ $op '' )-; + if ( driver_name eq 'Pg' ) { + qq-( $column $op '' )-; } else { - qq-( $_ $op "" )-; + qq-( $column $op "" )-; } } } else { - "$_ $op ?"; + "$column $op ?"; } - } @fields ); + } @real_fields ), + ( map { + my $op = '='; + my $column = $_; + if ( ref($record->{$_}) ) { + $op = $record->{$_}{'op'} if $record->{$_}{'op'}; + if ( uc($op) eq 'ILIKE' ) { + $op = 'LIKE'; + $record->{$_}{'value'} = lc($record->{$_}{'value'}); + $column = "LOWER($_)"; + } + $record->{$_} = $record->{$_}{'value'}; + } + + # ... EXISTS ( SELECT name, value FROM part_virtual_field + # JOIN virtual_field + # ON part_virtual_field.vfieldpart = virtual_field.vfieldpart + # WHERE recnum = svc_acct.svcnum + # AND (name, value) = ('egad', 'brain') ) + + my $value = $record->{$_}; + + my $subq; + + $subq = ($value ? 'EXISTS ' : 'NOT EXISTS ') . + "( SELECT part_virtual_field.name, virtual_field.value ". + "FROM part_virtual_field JOIN virtual_field ". + "ON part_virtual_field.vfieldpart = virtual_field.vfieldpart ". + "WHERE virtual_field.recnum = ${table}.${pkey} ". + "AND part_virtual_field.name = '${column}'". + ($value ? + " AND virtual_field.value ${op} '${value}'" + : "") . ")"; + $subq; + + } @virtual_fields ) ); + } + $statement .= " $extra_sql" if defined($extra_sql); warn "[debug]$me $statement\n" if $DEBUG > 1; @@ -250,10 +308,10 @@ sub qsearch { my $bind = 1; foreach my $field ( - grep defined( $record->{$_} ) && $record->{$_} ne '', @fields + grep defined( $record->{$_} ) && $record->{$_} ne '', @real_fields ) { if ( $record->{$field} =~ /^\d+(\.\d+)?$/ - && $dbdef->table($table)->column($field)->type =~ /(int)/i + && $dbdef->table($table)->column($field)->type =~ /(int|serial)/i ) { $sth->bind_param($bind++, $record->{$field}, { TYPE => SQL_INTEGER } ); } else { @@ -267,31 +325,64 @@ sub qsearch { $sth->execute or croak "Error executing \"$statement\": ". $sth->errstr; - $dbh->commit or croak $dbh->errstr if $FS::UID::AutoCommit; + my %result; + tie %result, "Tie::IxHash"; + @virtual_fields = "FS::$table"->virtual_fields; + my @stuff = @{ $sth->fetchall_arrayref( {} ) }; + if($pkey) { + %result = map { $_->{$pkey}, $_ } @stuff; + } else { + @result{@stuff} = @stuff; + } + + $sth->finish; + if ( keys(%result) and @virtual_fields ) { + $statement = + "SELECT virtual_field.recnum, part_virtual_field.name, ". + "virtual_field.value ". + "FROM part_virtual_field JOIN virtual_field USING (vfieldpart) ". + "WHERE part_virtual_field.dbtable = '$table' AND ". + "virtual_field.recnum IN (". + join(',', keys(%result)). ") AND part_virtual_field.name IN ('". + join(q!', '!, @virtual_fields) . "')"; + warn "[debug]$me $statement\n" if $DEBUG > 1; + $sth = $dbh->prepare($statement) or croak "$dbh->errstr doing $statement"; + $sth->execute or croak "Error executing \"$statement\": ". $sth->errstr; + + foreach (@{ $sth->fetchall_arrayref({}) }) { + my $recnum = $_->{recnum}; + my $name = $_->{name}; + my $value = $_->{value}; + if (exists($result{$recnum})) { + $result{$recnum}->{$name} = $value; + } + } + } + if ( eval 'scalar(@FS::'. $table. '::ISA);' ) { if ( eval 'FS::'. $table. '->can(\'new\')' eq \&new ) { #derivied class didn't override new method, so this optimization is safe if ( $cache ) { map { new_or_cached( "FS::$table", { %{$_} }, $cache ) - } @{$sth->fetchall_arrayref( {} )}; + } values(%result); } else { map { new( "FS::$table", { %{$_} } ) - } @{$sth->fetchall_arrayref( {} )}; + } values(%result); } } else { warn "untested code (class FS::$table uses custom new method)"; map { eval 'FS::'. $table. '->new( { %{$_} } )'; - } @{$sth->fetchall_arrayref( {} )}; + } values(%result); } } else { cluck "warning: FS::$table not loaded; returning FS::Record objects"; map { FS::Record->new( $table, { %{$_} } ); - } @{$sth->fetchall_arrayref( {} )}; + } values(%result); } } @@ -326,9 +417,11 @@ for a single item, or your data is corrupted. =cut sub qsearchs { # $result_record = &FS::Record:qsearchs('table',\%hash); + my $table = $_[0]; my(@result) = qsearch(@_); - carp "warning: Multiple records in scalar search!" if scalar(@result) > 1; - #should warn more vehemently if the search was on a primary key? + carp "warning: Multiple records in scalar search ($table)" + if scalar(@result) > 1; + #should warn more vehemently if the search was on a primary key? scalar(@result) ? ($result[0]) : (); } @@ -345,7 +438,7 @@ Returns the table name. =cut sub table { -# cluck "warning: FS::Record::table depriciated; supply one in subclass!"; +# cluck "warning: FS::Record::table deprecated; supply one in subclass!"; my $self = shift; $self -> {'Table'}; } @@ -412,11 +505,11 @@ sub AUTOLOAD { $field =~ s/.*://; if ( defined($value) ) { confess "errant AUTOLOAD $field for $self (arg $value)" - unless $self->can('setfield'); + unless ref($self) && $self->can('setfield'); $self->setfield($field,$value); } else { confess "errant AUTOLOAD $field for $self (no args)" - unless $self->can('getfield'); + unless ref($self) && $self->can('getfield'); $self->getfield($field); } } @@ -472,25 +565,41 @@ sub insert { return $error if $error; #single-field unique keys are given a value if false - #(like MySQL's AUTO_INCREMENT) + #(like MySQL's AUTO_INCREMENT or Pg SERIAL) foreach ( $self->dbdef_table->unique->singles ) { $self->unique($_) unless $self->getfield($_); } - #and also the primary key + + #and also the primary key, if the database isn't going to my $primary_key = $self->dbdef_table->primary_key; - $self->unique($primary_key) - if $primary_key && ! $self->getfield($primary_key); + my $db_seq = 0; + if ( $primary_key ) { + my $col = $self->dbdef_table->column($primary_key); + + $db_seq = + uc($col->type) eq 'SERIAL' + || ( driver_name eq 'Pg' + && defined($col->default) + && $col->default =~ /^nextval\(/i + ) + || ( driver_name eq 'mysql' + && defined($col->local) + && $col->local =~ /AUTO_INCREMENT/i + ); + $self->unique($primary_key) unless $self->getfield($primary_key) || $db_seq; + } + my $table = $self->table; #false laziness w/delete - my @fields = + my @real_fields = grep defined($self->getfield($_)) && $self->getfield($_) ne "", - $self->fields + real_fields($table) ; - my @values = map { _quote( $self->getfield($_), $self->table, $_) } @fields; + my @values = map { _quote( $self->getfield($_), $table, $_) } @real_fields; #eslaf - my $statement = "INSERT INTO ". $self->table. " ( ". - join( ', ', @fields ). + my $statement = "INSERT INTO $table ( ". + join( ', ', @real_fields ). ") VALUES (". join( ', ', @values ). ")" @@ -498,15 +607,6 @@ sub insert { warn "[debug]$me $statement\n" if $DEBUG > 1; my $sth = dbh->prepare($statement) or return dbh->errstr; - my $h_sth; - if ( defined $dbdef->table('h_'. $self->table) ) { - my $h_statement = $self->_h_statement('insert'); - warn "[debug]$me $h_statement\n" if $DEBUG > 2; - $h_sth = dbh->prepare($h_statement) or return dbh->errstr; - } else { - $h_sth = ''; - } - local $SIG{HUP} = 'IGNORE'; local $SIG{INT} = 'IGNORE'; local $SIG{QUIT} = 'IGNORE'; @@ -515,7 +615,92 @@ sub insert { local $SIG{PIPE} = 'IGNORE'; $sth->execute or return $sth->errstr; + + my $insertid = ''; + if ( $db_seq ) { # get inserted id from the database, if applicable + warn "[debug]$me retreiving sequence from database\n" if $DEBUG; + if ( driver_name eq 'Pg' ) { + + my $oid = $sth->{'pg_oid_status'}; + my $i_sql = "SELECT $primary_key FROM $table WHERE oid = ?"; + my $i_sth = dbh->prepare($i_sql) or do { + dbh->rollback if $FS::UID::AutoCommit; + return dbh->errstr; + }; + $i_sth->execute($oid) or do { + dbh->rollback if $FS::UID::AutoCommit; + return $i_sth->errstr; + }; + $insertid = $i_sth->fetchrow_arrayref->[0]; + + } elsif ( driver_name eq 'mysql' ) { + + $insertid = dbh->{'mysql_insertid'}; + # work around mysql_insertid being null some of the time, ala RT :/ + unless ( $insertid ) { + warn "WARNING: DBD::mysql didn't return mysql_insertid; ". + "using SELECT LAST_INSERT_ID();"; + my $i_sql = "SELECT LAST_INSERT_ID()"; + my $i_sth = dbh->prepare($i_sql) or do { + dbh->rollback if $FS::UID::AutoCommit; + return dbh->errstr; + }; + $i_sth->execute or do { + dbh->rollback if $FS::UID::AutoCommit; + return $i_sth->errstr; + }; + $insertid = $i_sth->fetchrow_arrayref->[0]; + } + + } else { + dbh->rollback if $FS::UID::AutoCommit; + return "don't know how to retreive inserted ids from ". driver_name. + ", try using counterfiles (maybe run dbdef-create?)"; + } + $self->setfield($primary_key, $insertid); + } + + my @virtual_fields = + grep defined($self->getfield($_)) && $self->getfield($_) ne "", + $self->virtual_fields; + if (@virtual_fields) { + my %v_values = map { $_, $self->getfield($_) } @virtual_fields; + + my $vfieldpart = $self->vfieldpart_hashref; + + my $v_statement = "INSERT INTO virtual_field(recnum, vfieldpart, value) ". + "VALUES (?, ?, ?)"; + + my $v_sth = dbh->prepare($v_statement) or do { + dbh->rollback if $FS::UID::AutoCommit; + return dbh->errstr; + }; + + foreach (keys(%v_values)) { + $v_sth->execute($self->getfield($primary_key), + $vfieldpart->{$_}, + $v_values{$_}) + or do { + dbh->rollback if $FS::UID::AutoCommit; + return $v_sth->errstr; + }; + } + } + + + my $h_sth; + if ( defined $dbdef->table('h_'. $table) ) { + my $h_statement = $self->_h_statement('insert'); + warn "[debug]$me $h_statement\n" if $DEBUG > 2; + $h_sth = dbh->prepare($h_statement) or do { + dbh->rollback if $FS::UID::AutoCommit; + return dbh->errstr; + }; + } else { + $h_sth = ''; + } $h_sth->execute or return $h_sth->errstr if $h_sth; + dbh->commit or croak dbh->errstr if $FS::UID::AutoCommit; ''; @@ -528,7 +713,7 @@ Depriciated (use insert instead). =cut sub add { - cluck "warning: FS::Record::add depriciated!"; + cluck "warning: FS::Record::add deprecated!"; insert @_; #call method in this scope } @@ -546,14 +731,14 @@ sub delete { map { $self->getfield($_) eq '' #? "( $_ IS NULL OR $_ = \"\" )" - ? ( driver_name =~ /^Pg$/i + ? ( driver_name eq 'Pg' ? "$_ IS NULL" : "( $_ IS NULL OR $_ = \"\" )" ) : "$_ = ". _quote($self->getfield($_),$self->table,$_) } ( $self->dbdef_table->primary_key ) ? ( $self->dbdef_table->primary_key) - : $self->fields + : real_fields($self->table) ); warn "[debug]$me $statement\n" if $DEBUG > 1; my $sth = dbh->prepare($statement) or return dbh->errstr; @@ -567,6 +752,19 @@ sub delete { $h_sth = ''; } + my $primary_key = $self->dbdef_table->primary_key; + my $v_sth; + my @del_vfields; + my $vfp = $self->vfieldpart_hashref; + foreach($self->virtual_fields) { + next if $self->getfield($_) eq ''; + unless(@del_vfields) { + my $st = "DELETE FROM virtual_field WHERE recnum = ? AND vfieldpart = ?"; + $v_sth = dbh->prepare($st) or return dbh->errstr; + } + push @del_vfields, $_; + } + local $SIG{HUP} = 'IGNORE'; local $SIG{INT} = 'IGNORE'; local $SIG{QUIT} = 'IGNORE'; @@ -577,6 +775,10 @@ sub delete { my $rc = $sth->execute or return $sth->errstr; #not portable #return "Record not found, statement:\n$statement" if $rc eq "0E0"; $h_sth->execute or return $h_sth->errstr if $h_sth; + $v_sth->execute($self->getfield($primary_key), $vfp->{$_}) + or return $v_sth->errstr + foreach (@del_vfields); + dbh->commit or croak dbh->errstr if $FS::UID::AutoCommit; #no need to needlessly destoy the data either (causes problems actually) @@ -592,7 +794,7 @@ Depriciated (use delete instead). =cut sub del { - cluck "warning: FS::Record::del depriciated!"; + cluck "warning: FS::Record::del deprecated!"; &delete(@_); #call method in this scope } @@ -604,7 +806,24 @@ returns the error, otherwise returns false. =cut sub replace { - my ( $new, $old ) = ( shift, shift ); + my $new = shift; + + my $old; + if ( @_ ) { + $old = shift; + } else { + warn "[debug]$me replace called with no arguments; autoloading old record\n" + if $DEBUG; + my $primary_key = $new->dbdef_table->primary_key; + if ( $primary_key ) { + $old = qsearchs($new->table, { $primary_key => $new->$primary_key() } ) + or croak "can't find ". $new->table. ".$primary_key ". + $new->$primary_key(); + } else { + croak $new->table. " has no primary key; pass old record as argument"; + } + } + warn "[debug]$me $new ->replace $old\n" if $DEBUG; return "Records not in same table!" unless $new->table eq $old->table; @@ -617,8 +836,11 @@ sub replace { my $error = $new->check; return $error if $error; - my @diff = grep $new->getfield($_) ne $old->getfield($_), $old->fields; - unless ( @diff ) { + #my @diff = grep $new->getfield($_) ne $old->getfield($_), $old->fields; + my %diff = map { ($new->getfield($_) ne $old->getfield($_)) + ? ($_, $new->getfield($_)) : () } $old->fields; + + unless ( keys(%diff) ) { carp "[warning]$me $new -> replace $old: records identical"; return ''; } @@ -626,18 +848,18 @@ sub replace { my $statement = "UPDATE ". $old->table. " SET ". join(', ', map { "$_ = ". _quote($new->getfield($_),$old->table,$_) - } @diff + } real_fields($old->table) ). ' WHERE '. join(' AND ', map { $old->getfield($_) eq '' #? "( $_ IS NULL OR $_ = \"\" )" - ? ( driver_name =~ /^Pg$/i - ? "$_ IS NULL" + ? ( driver_name eq 'Pg' + ? "( $_ IS NULL OR $_ = '' )" : "( $_ IS NULL OR $_ = \"\" )" ) : "$_ = ". _quote($old->getfield($_),$old->table,$_) - } ( $primary_key ? ( $primary_key ) : $old->fields ) + } ( $primary_key ? ( $primary_key ) : real_fields($old->table) ) ) ; warn "[debug]$me $statement\n" if $DEBUG > 1; @@ -661,6 +883,44 @@ sub replace { $h_new_sth = ''; } + # For virtual fields we have three cases with different SQL + # statements: add, replace, delete + my $v_add_sth; + my $v_rep_sth; + my $v_del_sth; + my (@add_vfields, @rep_vfields, @del_vfields); + my $vfp = $old->vfieldpart_hashref; + foreach(grep { exists($diff{$_}) } $new->virtual_fields) { + if($diff{$_} eq '') { + # Delete + unless(@del_vfields) { + my $st = "DELETE FROM virtual_field WHERE recnum = ? ". + "AND vfieldpart = ?"; + warn "[debug]$me $st\n" if $DEBUG > 2; + $v_del_sth = dbh->prepare($st) or return dbh->errstr; + } + push @del_vfields, $_; + } elsif($old->getfield($_) eq '') { + # Add + unless(@add_vfields) { + my $st = "INSERT INTO virtual_field (value, recnum, vfieldpart) ". + "VALUES (?, ?, ?)"; + warn "[debug]$me $st\n" if $DEBUG > 2; + $v_add_sth = dbh->prepare($st) or return dbh->errstr; + } + push @add_vfields, $_; + } else { + # Replace + unless(@rep_vfields) { + my $st = "UPDATE virtual_field SET value = ? ". + "WHERE recnum = ? AND vfieldpart = ?"; + warn "[debug]$me $st\n" if $DEBUG > 2; + $v_rep_sth = dbh->prepare($st) or return dbh->errstr; + } + push @rep_vfields, $_; + } + } + local $SIG{HUP} = 'IGNORE'; local $SIG{INT} = 'IGNORE'; local $SIG{QUIT} = 'IGNORE'; @@ -672,6 +932,24 @@ sub replace { #not portable #return "Record not found (or records identical)." if $rc eq "0E0"; $h_old_sth->execute or return $h_old_sth->errstr if $h_old_sth; $h_new_sth->execute or return $h_new_sth->errstr if $h_new_sth; + + $v_del_sth->execute($old->getfield($primary_key), + $vfp->{$_}) + or return $v_del_sth->errstr + foreach(@del_vfields); + + $v_add_sth->execute($new->getfield($_), + $old->getfield($primary_key), + $vfp->{$_}) + or return $v_add_sth->errstr + foreach(@add_vfields); + + $v_rep_sth->execute($new->getfield($_), + $old->getfield($primary_key), + $vfp->{$_}) + or return $v_rep_sth->errstr + foreach(@rep_vfields); + dbh->commit or croak dbh->errstr if $FS::UID::AutoCommit; ''; @@ -685,18 +963,34 @@ Depriciated (use replace instead). =cut sub rep { - cluck "warning: FS::Record::rep depriciated!"; + cluck "warning: FS::Record::rep deprecated!"; replace @_; #call method in this scope } =item check -Not yet implemented, croaks. Derived classes should provide a check method. +Checks virtual fields (using check_blocks). Subclasses should still provide +a check method to validate real fields, foreign keys, etc., and call this +method via $self->SUPER::check. + +(FIXME: Should this method try to make sure that it I<is> being called from +a subclass's check method, to keep the current semantics as far as possible?) =cut sub check { - confess "FS::Record::check not implemented; supply one in subclass!"; + #confess "FS::Record::check not implemented; supply one in subclass!"; + my $self = shift; + + foreach my $field ($self->virtual_fields) { + for ($self->getfield($field)) { + # See notes on check_block in FS::part_virtual_field. + eval $self->pvf($field)->check_block; + return $@ if $@; + $self->setfield($field, $_); + } + } + ''; } sub _h_statement { @@ -704,7 +998,7 @@ sub _h_statement { my @fields = grep defined($self->getfield($_)) && $self->getfield($_) ne "", - $self->fields + real_fields($self->table); ; my @values = map { _quote( $self->getfield($_), $self->table, $_) } @fields; @@ -718,8 +1012,13 @@ sub _h_statement { =item unique COLUMN -Replaces COLUMN in record with a unique number. Called by the B<add> method -on primary keys and single-field unique columns (see L<DBIx::DBSchema::Table>). +B<Warning>: External use is B<deprecated>. + +Replaces COLUMN in record with a unique number, using counters in the +filesystem. Used by the B<insert> method on single-field unique columns +(see L<DBIx::DBSchema::Table>) and also as a fallback for primary keys +that aren't SERIAL (Pg) or AUTO_INCREMENT (mysql). + Returns the new value. =cut @@ -728,8 +1027,6 @@ sub unique { my($self,$field) = @_; my($table)=$self->table; - #croak("&FS::UID::checkruid failed") unless &checkruid; - croak "Unique called on field $field, but it is ", $self->getfield($field), ", not null!" @@ -745,9 +1042,8 @@ sub unique { # my($counter) = new File::CounterFile "$user/$table.$field",0; # endhack - my($index)=$counter->inc; - $index=$counter->inc - while qsearchs($table,{$field=>$index}); #just in case + my $index = $counter->inc; + $index = $counter->inc while qsearchs($table, { $field=>$index } ); $index =~ /^(\d*)$/; $index=$1; @@ -774,6 +1070,21 @@ sub ut_float { ''; } +=item ut_snumber COLUMN + +Check/untaint signed numeric data (whole numbers). May not be null. If there +is an error, returns the error, otherwise returns false. + +=cut + +sub ut_snumber { + my($self, $field) = @_; + $self->getfield($field) =~ /^(-?)\s*(\d+)$/ + or return "Illegal or empty (numeric) $field: ". $self->getfield($field); + $self->setfield($field, "$1$2"); + ''; +} + =item ut_number COLUMN Check/untaint simple numeric data (whole numbers). May not be null. If there @@ -930,7 +1241,7 @@ sub ut_ip { $self->getfield($field) =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/ or return "Illegal (IP address) $field: ". $self->getfield($field); for ( $1, $2, $3, $4 ) { return "Illegal (IP address) $field" if $_ > 255; } - $self->setfield($field, "$1.$2.$3.$3"); + $self->setfield($field, "$1.$2.$3.$4"); ''; } @@ -1083,36 +1394,94 @@ sub ut_foreign_keyn { : ''; } + +=item virtual_fields [ TABLE ] + +Returns a list of virtual fields defined for the table. This should not +be exported, and should only be called as an instance or class method. + +=cut + +sub virtual_fields { + my $self = shift; + my $table; + $table = $self->table or confess "virtual_fields called on non-table"; + + confess "Unknown table $table" unless $dbdef->table($table); + + return () unless $self->dbdef->table('part_virtual_field'); + + unless ( $virtual_fields_cache{$table} ) { + my $query = 'SELECT name from part_virtual_field ' . + "WHERE dbtable = '$table'"; + my $dbh = dbh; + my $result = $dbh->selectcol_arrayref($query); + confess $dbh->errstr if $dbh->err; + $virtual_fields_cache{$table} = $result; + } + + @{$virtual_fields_cache{$table}}; + +} + + =item fields [ TABLE ] -This can be used as both a subroutine and a method call. It returns a list -of the columns in this record's table, or an explicitly specified table. -(See L<DBIx::DBSchema::Table>). +This is a wrapper for real_fields and virtual_fields. Code that called +fields before should probably continue to call fields. =cut -# Usage: @fields = fields($table); -# @fields = $record->fields; sub fields { my $something = shift; my $table; - if ( ref($something) ) { + if($something->isa('FS::Record')) { $table = $something->table; } else { $table = $something; + $something = "FS::$table"; } - #croak "Usage: \@fields = fields(\$table)\n or: \@fields = \$record->fields" unless $table; - my($table_obj) = $dbdef->table($table); - confess "Unknown table $table" unless $table_obj; - $table_obj->columns; + return (real_fields($table), $something->virtual_fields()); } =back +=item pvf FIELD_NAME + +Returns the FS::part_virtual_field object corresponding to a field in the +record (specified by FIELD_NAME). + +=cut + +sub pvf { + my ($self, $name) = (shift, shift); + + if(grep /^$name$/, $self->virtual_fields) { + return qsearchs('part_virtual_field', { dbtable => $self->table, + name => $name } ); + } + '' +} + =head1 SUBROUTINES =over 4 +=item real_fields [ TABLE ] + +Returns a list of the real columns in the specified table. Called only by +fields() and other subroutines elsewhere in FS::Record. + +=cut + +sub real_fields { + my $table = shift; + + my($table_obj) = $dbdef->table($table); + confess "Unknown table $table" unless $table_obj; + $table_obj->columns; +} + =item reload_dbdef([FILENAME]) Load a database definition (see L<DBIx::DBSchema>), optionally from a @@ -1151,28 +1520,60 @@ type (see L<DBIx::DBSchema::Column>) does not end in `char' or `binary'. =cut sub _quote { - my($value,$table,$field)=@_; - my($dbh)=dbh; - if ( $value =~ /^\d+(\.\d+)?$/ && -# ! ( datatype($table,$field) =~ /^char/ ) - ! $dbdef->table($table)->column($field)->type =~ /(char|binary|text)$/i - ) { + my($value, $table, $column) = @_; + my $column_obj = $dbdef->table($table)->column($column); + my $column_type = $column_obj->type; + + if ( $value eq '' && $column_type =~ /^int/ ) { + if ( $column_obj->null ) { + 'NULL'; + } else { + cluck "WARNING: Attempting to set non-null integer $table.$column null; ". + "using 0 instead"; + 0; + } + } elsif ( $value =~ /^\d+(\.\d+)?$/ && + ! $column_type =~ /(char|binary|text)$/i ) { $value; } else { - $dbh->quote($value); + dbh->quote($value); } } +=item vfieldpart_hashref TABLE + +Returns a hashref of virtual field names and vfieldparts applicable to the given +TABLE. + +=cut + +sub vfieldpart_hashref { + my $self = shift; + my $table = $self->table; + + return {} unless $self->dbdef->table('part_virtual_field'); + + my $dbh = dbh; + my $statement = "SELECT vfieldpart, name FROM part_virtual_field WHERE ". + "dbtable = '$table'"; + my $sth = $dbh->prepare($statement); + $sth->execute or croak "Execution of '$statement' failed: ".$dbh->errstr; + return { map { $_->{name}, $_->{vfieldpart} } + @{$sth->fetchall_arrayref({})} }; + +} + + =item hfields TABLE -This is depriciated. Don't use it. +This is deprecated. Don't use it. It returns a hash-type list with the fields of this record's table set true. =cut sub hfields { - carp "warning: hfields is depriciated"; + carp "warning: hfields is deprecated"; my($table)=@_; my(%hash); foreach (fields($table)) { @@ -1208,7 +1609,7 @@ sub DESTROY { return; } This module should probably be renamed, since much of the functionality is of general use. It is not completely unlike Adapter::DBI (see below). -Exported qsearch and qsearchs should be depriciated in favor of method calls +Exported qsearch and qsearchs should be deprecated in favor of method calls (against an FS::Record object like the old search and searchs that qsearch and qsearchs were on top of.) @@ -1216,7 +1617,7 @@ The whole fields / hfields mess should be removed. The various WHERE clauses should be subroutined. -table string should be depriciated in favor of DBIx::DBSchema::Table. +table string should be deprecated in favor of DBIx::DBSchema::Table. No doubt we could benefit from a Tied hash. Documenting how exists / defined true maps to the database (and WHERE clauses) would also help. diff --git a/FS/FS/UID.pm b/FS/FS/UID.pm index 0b10612c5..8271f89f2 100644 --- a/FS/FS/UID.pm +++ b/FS/FS/UID.pm @@ -3,8 +3,8 @@ package FS::UID; use strict; use vars qw( @ISA @EXPORT_OK $cgi $dbh $freeside_uid $user - $conf_dir $secrets $datasrc $db_user $db_pass %callback $driver_name - $AutoCommit + $conf_dir $secrets $datasrc $db_user $db_pass %callback @callback + $driver_name $AutoCommit ); use subs qw( getsecrets cgisetotaker @@ -95,9 +95,33 @@ sub forksuidsetup { # breaks multi-database installs # delete $callback{$_}; #run once } + &{$_} foreach @callback; + $dbh; } +=item install_callback + +A package can install a callback to be run in adminsuidsetup by passing +a coderef to the FS::UID->install_callback class method. If adminsuidsetup has +run already, the callback will also be run immediately. + + $coderef = sub { warn "Hi, I'm returning your call!" }; + FS::UID->install_callback($coderef); + + install_callback FS::UID sub { + warn "Hi, I'm returning your call!" + }; + +=cut + +sub install_callback { + my $class = shift; + my $callback = shift; + push @callback, $callback; + &{$callback} if $dbh; +} + =item cgisuidsetup CGI_object Takes a single argument, which is a CGI (see L<CGI>) or Apache (see L<Apache>) @@ -246,17 +270,28 @@ sub getsecrets { =head1 CALLBACKS -Warning: this interface is likely to change in future releases. +Warning: this interface is (still) likely to change in future releases. -A package can install a callback to be run in adminsuidsetup by putting a -coderef into the hash %FS::UID::callback : +New (experimental) callback interface: + +A package can install a callback to be run in adminsuidsetup by passing +a coderef to the FS::UID->install_callback class method. If adminsuidsetup has +run already, the callback will also be run immediately. $coderef = sub { warn "Hi, I'm returning your call!" }; - $FS::UID::callback{'Package::Name'}; + FS::UID->install_callback($coderef); + + install_callback FS::UID sub { + warn "Hi, I'm returning your call!" + }; -=head1 VERSION +Old (deprecated) callback interface: -$Id: UID.pm,v 1.18 2002-07-03 11:23:25 ivan Exp $ +A package can install a callback to be run in adminsuidsetup by putting a +coderef into the hash %FS::UID::callback : + + $coderef = sub { warn "Hi, I'm returning your call!" }; + $FS::UID::callback{'Package::Name'} = $coderef; =head1 BUGS @@ -269,7 +304,7 @@ cgisuidsetup will go away as well. Goes through contortions to support non-OO syntax with multiple datasrc's. -Callbacks are inelegant. +Callbacks are (still) inelegant. =head1 SEE ALSO diff --git a/FS/FS/acct_snarf.pm b/FS/FS/acct_snarf.pm new file mode 100644 index 000000000..b4e88bfc9 --- /dev/null +++ b/FS/FS/acct_snarf.pm @@ -0,0 +1,128 @@ +package FS::acct_snarf; + +use strict; +use vars qw( @ISA ); +use FS::Record; + +@ISA = qw( FS::Record ); + +=head1 NAME + +FS::acct_snarf - Object methods for acct_snarf records + +=head1 SYNOPSIS + + use FS::acct_snarf; + + $record = new FS::acct_snarf \%hash; + $record = new FS::acct_snarf { 'column' => 'value' }; + + $error = $record->insert; + + $error = $new_record->replace($old_record); + + $error = $record->delete; + + $error = $record->check; + +=head1 DESCRIPTION + +An FS::svc_acct object represents an external mail account, typically for +download of mail. FS::acct_snarf inherits from FS::Record. The following +fields are currently supported: + +=over 4 + +=item snarfnum - primary key + +=item svcnum - Account (see L<FS::svc_acct>) + +=item machine - external machine to download mail from + +=item protocol - protocol (pop3, imap, etc.) + +=item username - external login username + +=item _password - external login password + +=back + +=head1 METHODS + +=over 4 + +=item new HASHREF + +Creates a new record. To add the record to the database, see L<"insert">. + +Note that this stores the hash reference, not a distinct copy of the hash it +points to. You can ask the object for a copy with the I<hash> method. + +=cut + +sub table { 'acct_snarf'; } + +=item insert + +Adds this record to the database. If there is an error, returns the error, +otherwise returns false. + +=cut + +# the insert method can be inherited from FS::Record + +=item delete + +Delete this record from the database. + +=cut + +# the delete method can be inherited from FS::Record + +=item replace OLD_RECORD + +Replaces the OLD_RECORD with this one in the database. If there is an error, +returns the error, otherwise returns false. + +=cut + +# the replace method can be inherited from FS::Record + +=item check + +Checks all fields to make sure this is a valid external mail account. If +there is an error, returns the error, otherwise returns false. Called by the +insert and replace methods. + +=cut + +sub check { + my $self = shift; + my $error = + $self->ut_numbern('snarfnum') + || $self->ut_number('svcnum') + || $self->ut_foreign_key('svcnum', 'svc_acct', 'svcnum') + || $self->ut_domain('machine') + || $self->ut_alphan('protocol') + || $self->ut_textn('username') + ; + return $error if $error; + + $self->_password =~ /^[^\t\n]*$/ or return "illegal password"; + $self->_password($1); + + ''; #no error +} + +=back + +=head1 BUGS + +=head1 SEE ALSO + +L<FS::Record>, schema.html from the base documentation. + +=cut + +1; + diff --git a/FS/FS/addr_block.pm b/FS/FS/addr_block.pm new file mode 100755 index 000000000..1fb60606d --- /dev/null +++ b/FS/FS/addr_block.pm @@ -0,0 +1,331 @@ +package FS::addr_block; + +use strict; +use vars qw( @ISA ); +use FS::Record qw( qsearchs qsearch dbh ); +use FS::router; +use FS::svc_broadband; +use FS::Conf; +use NetAddr::IP; + +@ISA = qw( FS::Record ); + +=head1 NAME + +FS::addr_block - Object methods for addr_block records + +=head1 SYNOPSIS + + use FS::addr_block; + + $record = new FS::addr_block \%hash; + $record = new FS::addr_block { 'column' => 'value' }; + + $error = $record->insert; + + $error = $new_record->replace($old_record); + + $error = $record->delete; + + $error = $record->check; + +=head1 DESCRIPTION + +An FS::addr_block record describes an address block assigned for broadband +access. FS::addr_block inherits from FS::Record. The following fields are +currently supported: + +=over 4 + +=item blocknum - primary key, used in FS::svc_broadband to associate +services to the block. + +=item routernum - the router (see FS::router) to which this +block is assigned. + +=item ip_gateway - the gateway address used by customers within this block. + +=item ip_netmask - the netmask of the block, expressed as an integer. + +=back + +=head1 METHODS + +=over 4 + +=item new HASHREF + +Create a new record. To add the record to the database, see "insert". + +=cut + +sub table { 'addr_block'; } + +=item insert + +Adds this record to the database. If there is an error, returns the error, +otherwise returns false. + +=item delete + +Deletes this record from the database. If there is an error, returns the +error, otherwise returns false. + +sub delete { + my $self = shift; + return 'Block must be deallocated before deletion' + if $self->router; + + $self->SUPER::delete; +} + +=item replace OLD_RECORD + +Replaces OLD_RECORD with this one in the database. If there is an error, +returns the error, otherwise returns false. + +=item check + +Checks all fields to make sure this is a valid record. If there is an error, +returns the error, otherwise returns false. Called by the insert and replace +methods. + +=cut + +sub check { + my $self = shift; + + my $error = + $self->ut_number('routernum') + || $self->ut_ip('ip_gateway') + || $self->ut_number('ip_netmask') + ; + return $error if $error; + + + # A routernum of 0 indicates an unassigned block and is allowed + return "Unknown routernum" + if ($self->routernum and not $self->router); + + my $self_addr = $self->NetAddr; + return "Cannot parse address: ". $self->ip_gateway . '/' . $self->ip_netmask + unless $self_addr; + + if (not $self->blocknum) { + my @block = grep { + my $block_addr = $_->NetAddr; + if($block_addr->contains($self_addr) + or $self_addr->contains($block_addr)) { $_; }; + } qsearch( 'addr_block', {}); + foreach(@block) { + return "Block intersects existing block ".$_->ip_gateway."/".$_->ip_netmask; + } + } + + $self->SUPER::check; +} + + +=item router + +Returns the FS::router object corresponding to this object. If the +block is unassigned, returns undef. + +=cut + +sub router { + my $self = shift; + return qsearchs('router', { routernum => $self->routernum }); +} + +=item svc_broadband + +Returns a list of FS::svc_broadband objects associated +with this object. + +=cut + +sub svc_broadband { + my $self = shift; + return qsearch('svc_broadband', { blocknum => $self->blocknum }); +} + +=item NetAddr + +Returns a NetAddr::IP object for this block's address and netmask. + +=cut + +sub NetAddr { + my $self = shift; + + return new NetAddr::IP ($self->ip_gateway, $self->ip_netmask); +} + +=item next_free_addr + +Returns a NetAddr::IP object corresponding to the first unassigned address +in the block (other than the network, broadcast, or gateway address). If +there are no free addresses, returns false. + +=cut + +sub next_free_addr { + my $self = shift; + + my $conf = new FS::Conf; + my @excludeaddr = $conf->config('exclude_ip_addr'); + +my @used = +( (map { $_->NetAddr->addr } + ($self, + qsearch('svc_broadband', { blocknum => $self->blocknum })) + ), @excludeaddr +); + + my @free = $self->NetAddr->hostenum; + while (my $ip = shift @free) { + if (not grep {$_ eq $ip->addr;} @used) { return $ip; }; + } + + ''; + +} + +=item allocate + +Allocates this address block to a router. Takes an FS::router object +as an argument. + +At present it's not possible to reallocate a block to a different router +except by deallocating it first, which requires that none of its addresses +be assigned. This is probably as it should be. + +=cut + +sub allocate { + my ($self, $router) = @_; + + return 'Block is already allocated' + if($self->router); + + return 'Block must be allocated to a router' + unless(ref $router eq 'FS::router'); + + my @svc = $self->svc_broadband; + if (@svc) { + return 'Block has assigned addresses: '. join ', ', map {$_->ip_addr} @svc; + } + + my $new = new FS::addr_block {$self->hash}; + $new->routernum($router->routernum); + return $new->replace($self); + +} + +=item deallocate + +Deallocates the block (i.e. sets the routernum to 0). If any addresses in the +block are assigned to services, it fails. + +=cut + +sub deallocate { + my $self = shift; + + my @svc = $self->svc_broadband; + if (@svc) { + return 'Block has assigned addresses: '. join ', ', map {$_->ip_addr} @svc; + } + + my $new = new FS::addr_block {$self->hash}; + $new->routernum(0); + return $new->replace($self); +} + +=item split_block + +Splits this address block into two equal blocks, occupying the same space as +the original block. The first of the two will also have the same blocknum. +The gateway address of each block will be set to the first usable address, i.e. +(network address)+1. Since this method is designed for use on unallocated +blocks, this is probably the correct behavior. + +(At present, splitting allocated blocks is disallowed. Anyone who wants to +implement this is reminded that each split costs three addresses, and any +customers who were using these addresses will have to be moved; depending on +how full the block was before being split, they might have to be moved to a +different block. Anyone who I<still> wants to implement it is asked to tie it +to a configuration switch so that site admins can disallow it.) + +=cut + +sub split_block { + + # We should consider using Attribute::Handlers/Aspect/Hook::LexWrap/ + # something to atomicize functions, so that we can say + # + # sub split_block : atomic { + # + # instead of repeating all this AutoCommit verbage in every + # sub that does more than one database operation. + + my $oldAutoCommit = $FS::UID::AutoCommit; + local $FS::UID::AutoCommit = 0; + my $dbh = dbh; + + my $self = shift; + my $error; + + if ($self->router) { + return 'Block is already allocated'; + } + + #TODO: Smallest allowed block should be a config option. + if ($self->NetAddr->masklen() ge 30) { + return 'Cannot split blocks with a mask length >= 30'; + } + + my (@new, @ip); + $ip[0] = $self->NetAddr; + @ip = map {$_->first()} $ip[0]->split($self->ip_netmask + 1); + + foreach (0,1) { + $new[$_] = new FS::addr_block {$self->hash}; + $new[$_]->ip_gateway($ip[$_]->addr); + $new[$_]->ip_netmask($ip[$_]->masklen); + } + + $new[1]->blocknum(''); + + $error = $new[0]->replace($self); + if ($error) { + $dbh->rollback; + return $error; + } + + $error = $new[1]->insert; + if ($error) { + $dbh->rollback; + return $error; + } + + $dbh->commit or die $dbh->errstr if $oldAutoCommit; + return ''; +} + +=item merge + +To be implemented. + +=back + +=head1 BUGS + +Minimum block size should be a config option. It's hardcoded at /30 right +now because that's the smallest block that makes any sense at all. + +=cut + +1; + diff --git a/FS/FS/agent.pm b/FS/FS/agent.pm index f11a28db9..2f70d654d 100644 --- a/FS/FS/agent.pm +++ b/FS/FS/agent.pm @@ -50,6 +50,12 @@ from FS::Record. The following fields are currently supported: =item freq - For future use. +=item disabled - Disabled flag, empty or 'Y' + +=item username - Username for the Agent interface + +=item _password - Password for the Agent interface + =back =head1 METHODS @@ -110,11 +116,28 @@ sub check { ; return $error if $error; + if ( $self->dbdef_table->column('disabled') ) { + $error = $self->ut_enum('disabled', [ '', 'Y' ] ); + return $error if $error; + } + + if ( $self->dbdef_table->column('username') ) { + $error = $self->ut_alphan('username'); + return $error if $error; + if ( length($self->username) ) { + my $conflict = qsearchs('agent', { 'username' => $self->username } ); + return 'duplicate agent username (with '. $conflict->agent. ')' + if $conflict; + $error = $self->ut_text('password'); # ut_text... arbitrary choice + } else { + $self->_password(''); + } + } + return "Unknown typenum!" unless $self->agent_type; - ''; - + $self->SUPER::check; } =item agent_type @@ -145,7 +168,7 @@ sub pkgpart_hashref { =head1 VERSION -$Id: agent.pm,v 1.3 2002-03-24 18:23:47 ivan Exp $ +$Id: agent.pm,v 1.6 2003-09-30 15:01:46 ivan Exp $ =head1 BUGS diff --git a/FS/FS/agent_type.pm b/FS/FS/agent_type.pm index 988533ae3..5ba5ef291 100644 --- a/FS/FS/agent_type.pm +++ b/FS/FS/agent_type.pm @@ -102,7 +102,8 @@ sub check { my $self = shift; $self->ut_numbern('typenum') - or $self->ut_text('atype'); + or $self->ut_text('atype') + or $self->SUPER::check; } @@ -150,7 +151,7 @@ sub pkgpart { =head1 VERSION -$Id: agent_type.pm,v 1.1 1999-08-04 09:03:53 ivan Exp $ +$Id: agent_type.pm,v 1.2 2003-08-05 00:20:40 khoff Exp $ =head1 BUGS diff --git a/FS/FS/cust_bill.pm b/FS/FS/cust_bill.pm index 5a9fdd09b..a3e76620e 100644 --- a/FS/FS/cust_bill.pm +++ b/FS/FS/cust_bill.pm @@ -2,16 +2,12 @@ package FS::cust_bill; use strict; use vars qw( @ISA $conf $money_char ); -use vars qw( $lpr $invoice_from $smtpmachine ); -use vars qw( $processor ); -use vars qw( $xaction $E_NoErr ); -use vars qw( $bop_processor $bop_login $bop_password $bop_action @bop_options ); use vars qw( $invoice_lines @buf ); #yuck use Date::Format; -use Mail::Internet 1.44; -use Mail::Header; use Text::Template; +use FS::UID qw( datasrc ); use FS::Record qw( qsearch qsearchs ); +use FS::Misc qw( send_email ); use FS::cust_main; use FS::cust_bill_pkg; use FS::cust_credit; @@ -24,50 +20,10 @@ use FS::cust_bill_event; @ISA = qw( FS::Record ); #ask FS::UID to run this stuff for us later -$FS::UID::callback{'FS::cust_bill'} = sub { - +FS::UID->install_callback( sub { $conf = new FS::Conf; - $money_char = $conf->config('money_char') || '$'; - - $lpr = $conf->config('lpr'); - $invoice_from = $conf->config('invoice_from'); - $smtpmachine = $conf->config('smtpmachine'); - - if ( $conf->exists('cybercash3.2') ) { - require CCMckLib3_2; - #qw($MCKversion %Config InitConfig CCError CCDebug CCDebug2); - require CCMckDirectLib3_2; - #qw(SendCC2_1Server); - require CCMckErrno3_2; - #qw(MCKGetErrorMessage $E_NoErr); - import CCMckErrno3_2 qw($E_NoErr); - - my $merchant_conf; - ($merchant_conf,$xaction)= $conf->config('cybercash3.2'); - my $status = &CCMckLib3_2::InitConfig($merchant_conf); - if ( $status != $E_NoErr ) { - warn "CCMckLib3_2::InitConfig error:\n"; - foreach my $key (keys %CCMckLib3_2::Config) { - warn " $key => $CCMckLib3_2::Config{$key}\n" - } - my($errmsg) = &CCMckErrno3_2::MCKGetErrorMessage($status); - die "CCMckLib3_2::InitConfig fatal error: $errmsg\n"; - } - $processor='cybercash3.2'; - } elsif ( $conf->exists('business-onlinepayment') ) { - ( $bop_processor, - $bop_login, - $bop_password, - $bop_action, - @bop_options - ) = $conf->config('business-onlinepayment'); - $bop_action ||= 'normal authorization'; - eval "use Business::OnlinePayment"; - $processor="Business::OnlinePayment::$bop_processor"; - } - -}; +} ); =head1 NAME @@ -205,7 +161,7 @@ sub check { $self->printed(0) if $self->printed eq ''; - ''; #no error + $self->SUPER::check; } =item previous @@ -372,32 +328,27 @@ sub send { my @print_text = $self->print_text('', $template); my @invoicing_list = $self->cust_main->invoicing_list; - if ( grep { $_ ne 'POST' } @invoicing_list ) { #email invoice - #false laziness w/FS::cust_pay::delete & fs_signup_server && ::realtime_card - #$ENV{SMTPHOSTS} = $smtpmachine; - $ENV{MAILADDRESS} = $invoice_from; - my $header = new Mail::Header ( [ - "From: $invoice_from", - "To: ". join(', ', grep { $_ ne 'POST' } @invoicing_list ), - "Sender: $invoice_from", - "Reply-To: $invoice_from", - "Date: ". time2str("%a, %d %b %Y %X %z", time), - "Subject: Invoice", - ] ); - my $message = new Mail::Internet ( - 'Header' => $header, - 'Body' => [ @print_text ], #( date) + if ( grep { $_ ne 'POST' } @invoicing_list or !@invoicing_list ) { #email + + #better to notify this person than silence + @invoicing_list = ($conf->config('invoice_from')) unless @invoicing_list; + + my $error = send_email( + 'from' => $conf->config('invoice_from'), + 'to' => [ grep { $_ ne 'POST' } @invoicing_list ], + 'subject' => 'Invoice', + 'body' => \@print_text, ); - $!=0; - $message->smtpsend( Host => $smtpmachine ) - or $message->smtpsend( Host => $smtpmachine, Debug => 1 ) - or return "(customer # ". $self->custnum. ") can't send invoice email". - " to ". join(', ', grep { $_ ne 'POST' } @invoicing_list ). - " via server $smtpmachine with SMTP: $!"; + return "can't send invoice: $error" if $error; + + } + if ( $conf->config('invoice_latex') ) { + @print_text = $self->print_ps('', $template); } - if ( ! @invoicing_list || grep { $_ eq 'POST' } @invoicing_list ) { #postal + if ( grep { $_ eq 'POST' } @invoicing_list ) { #postal + my $lpr = $conf->config('lpr'); open(LPR, "|$lpr") or return "Can't open pipe to $lpr: $!"; print LPR @print_text; @@ -410,6 +361,173 @@ sub send { } +=item send_csv OPTIONS + +Sends invoice as a CSV data-file to a remote host with the specified protocol. + +Options are: + +protocol - currently only "ftp" +server +username +password +dir + +The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number +and YYMMDDHHMMSS is a timestamp. + +The fields of the CSV file is as follows: + +record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate + +=over 4 + +=item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg> + +If B<record_type> is C<cust_bill>, this is a primary invoice record. The +last five fields (B<pkg> through B<edate>) are irrelevant, and all other +fields are filled in. + +If B<record_type> is C<cust_bill_pkg>, this is a line item record. Only the +first two fields (B<record_type> and B<invnum>) and the last five fields +(B<pkg> through B<edate>) are filled in. + +=item invnum - invoice number + +=item custnum - customer number + +=item _date - invoice date + +=item charged - total invoice amount + +=item first - customer first name + +=item last - customer first name + +=item company - company name + +=item address1 - address line 1 + +=item address2 - address line 1 + +=item city + +=item state + +=item zip + +=item country + +=item pkg - line item description + +=item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined) + +=item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined) + +=item sdate - start date for recurring fee + +=item edate - end date for recurring fee + +=back + +=cut + +sub send_csv { + my($self, %opt) = @_; + + #part one: create file + + my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill"; + mkdir $spooldir, 0700 unless -d $spooldir; + + my $file = $spooldir. '/'. $self->invnum. time2str('-%Y%m%d%H%M%S.csv', time); + + open(CSV, ">$file") or die "can't open $file: $!"; + + eval "use Text::CSV_XS"; + die $@ if $@; + + my $csv = Text::CSV_XS->new({'always_quote'=>1}); + + my $cust_main = $self->cust_main; + + $csv->combine( + 'cust_bill', + $self->invnum, + $self->custnum, + time2str("%x", $self->_date), + sprintf("%.2f", $self->charged), + ( map { $cust_main->getfield($_) } + qw( first last company address1 address2 city state zip country ) ), + map { '' } (1..5), + ) or die "can't create csv"; + print CSV $csv->string. "\n"; + + #new charges (false laziness w/print_text) + foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) { + + my($pkg, $setup, $recur, $sdate, $edate); + if ( $cust_bill_pkg->pkgnum ) { + + ($pkg, $setup, $recur, $sdate, $edate) = ( + $cust_bill_pkg->cust_pkg->part_pkg->pkg, + ( $cust_bill_pkg->setup != 0 + ? sprintf("%.2f", $cust_bill_pkg->setup ) + : '' ), + ( $cust_bill_pkg->recur != 0 + ? sprintf("%.2f", $cust_bill_pkg->recur ) + : '' ), + time2str("%x", $cust_bill_pkg->sdate), + time2str("%x", $cust_bill_pkg->edate), + ); + + } else { #pkgnum tax + next unless $cust_bill_pkg->setup != 0; + my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc') + ? ( $cust_bill_pkg->itemdesc || 'Tax' ) + : 'Tax'; + ($pkg, $setup, $recur, $sdate, $edate) = + ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' ); + } + + $csv->combine( + 'cust_bill_pkg', + $self->invnum, + ( map { '' } (1..11) ), + ($pkg, $setup, $recur, $sdate, $edate) + ) or die "can't create csv"; + print CSV $csv->string. "\n"; + + } + + close CSV or die "can't close CSV: $!"; + + #part two: upload it + + my $net; + if ( $opt{protocol} eq 'ftp' ) { + eval "use Net::FTP;"; + die $@ if $@; + $net = Net::FTP->new($opt{server}) or die @$; + } else { + die "unknown protocol: $opt{protocol}"; + } + + $net->login( $opt{username}, $opt{password} ) + or die "can't FTP to $opt{username}\@$opt{server}: login error: $@"; + + $net->binary or die "can't set binary mode"; + + $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}"; + + $net->put($file) or die "can't put $file: $!"; + + $net->quit; + + unlink $file; + +} + =item comp Pays this invoice with a compliemntary payment. If there is an error, @@ -432,53 +550,54 @@ sub comp { =item realtime_card -Attempts to pay this invoice with a Business::OnlinePayment realtime gateway. -See http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment -for supproted processors. +Attempts to pay this invoice with a credit card payment via a +Business::OnlinePayment realtime gateway. See +http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment +for supported processors. =cut sub realtime_card { my $self = shift; - my $cust_main = $self->cust_main; - my $amount = $self->owed; + $self->realtime_bop( 'CC', @_ ); +} - unless ( $processor =~ /^Business::OnlinePayment::(.*)$/ ) { - return "Real-time card processing not enabled (processor $processor)"; - } - my $bop_processor = $1; #hmm? - - my $address = $cust_main->address1; - $address .= ", ". $cust_main->address2 if $cust_main->address2; - - #fix exp. date - #$cust_main->paydate =~ /^(\d+)\/\d*(\d{2})$/; - $cust_main->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/; - my $exp = "$2/$1"; - - my($payname, $payfirst, $paylast); - if ( $cust_main->payname ) { - $payname = $cust_main->payname; - $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/ - or do { - #$dbh->rollback if $oldAutoCommit; - return "Illegal payname $payname"; - }; - ($payfirst, $paylast) = ($1, $2); - } else { - $payfirst = $cust_main->getfield('first'); - $paylast = $cust_main->getfield('last'); - $payname = "$payfirst $paylast"; - } +=item realtime_ach - my @invoicing_list = grep { $_ ne 'POST' } $cust_main->invoicing_list; - if ( $conf->exists('emailinvoiceauto') - || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) { - push @invoicing_list, $cust_main->default_invoicing_list; - } - my $email = $invoicing_list[0]; +Attempts to pay this invoice with an electronic check (ACH) payment via a +Business::OnlinePayment realtime gateway. See +http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment +for supported processors. - my( $action1, $action2 ) = split(/\s*\,\s*/, $bop_action ); +=cut + +sub realtime_ach { + my $self = shift; + $self->realtime_bop( 'ECHECK', @_ ); +} + +=item realtime_lec + +Attempts to pay this invoice with phone bill (LEC) payment via a +Business::OnlinePayment realtime gateway. See +http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment +for supported processors. + +=cut + +sub realtime_lec { + my $self = shift; + $self->realtime_bop( 'LEC', @_ ); +} + +sub realtime_bop { + my( $self, $method ) = @_; + + my $cust_main = $self->cust_main; + my $balance = $cust_main->balance; + my $amount = ( $balance < $self->owed ) ? $balance : $self->owed; + $amount = sprintf("%.2f", $amount); + return "not run (balance $balance)" unless $amount > 0; my $description = 'Internet Services'; if ( $conf->exists('business-onlinepayment-description') ) { @@ -493,211 +612,13 @@ sub realtime_card { grep { $_->pkgnum } $self->cust_bill_pkg ); $description = eval qq("$dtempl"); - - } - - my $transaction = - new Business::OnlinePayment( $bop_processor, @bop_options ); - $transaction->content( - 'type' => 'CC', - 'login' => $bop_login, - 'password' => $bop_password, - 'action' => $action1, - 'description' => $description, - 'amount' => $amount, - 'invoice_number' => $self->invnum, - 'customer_id' => $self->custnum, - 'last_name' => $paylast, - 'first_name' => $payfirst, - 'name' => $payname, - 'address' => $address, - 'city' => $cust_main->city, - 'state' => $cust_main->state, - 'zip' => $cust_main->zip, - 'country' => $cust_main->country, - 'card_number' => $cust_main->payinfo, - 'expiration' => $exp, - 'referer' => 'http://cleanwhisker.420.am/', - 'email' => $email, - 'phone' => $cust_main->daytime || $cust_main->night, - ); - $transaction->submit(); - - if ( $transaction->is_success() && $action2 ) { - my $auth = $transaction->authorization; - my $ordernum = $transaction->order_number; - - #warn "********* $auth ***********\n"; - #warn "********* $ordernum ***********\n"; - my $capture = - new Business::OnlinePayment( $bop_processor, @bop_options ); - - $capture->content( - action => $action2, - login => $bop_login, - password => $bop_password, - order_number => $ordernum, - amount => $amount, - authorization => $auth, - description => $description, - ); - - $capture->submit(); - - unless ( $capture->is_success ) { - my $e = "Authorization sucessful but capture failed, invnum #". - $self->invnum. ': '. $capture->result_code. - ": ". $capture->error_message; - warn $e; - return $e; - } - } - if ( $transaction->is_success() ) { - - my $cust_pay = new FS::cust_pay ( { - 'invnum' => $self->invnum, - 'paid' => $amount, - '_date' => '', - 'payby' => 'CARD', - 'payinfo' => $cust_main->payinfo, - 'paybatch' => "$processor:". $transaction->authorization, - } ); - my $error = $cust_pay->insert; - if ( $error ) { - # gah, even with transactions. - my $e = 'WARNING: Card debited but database not updated - '. - 'error applying payment, invnum #' . $self->invnum. - " ($processor): $error"; - warn $e; - return $e; - } else { - return ''; - } - #} elsif ( $options{'report_badcard'} ) { - } else { - - my $perror = "$processor error, invnum #". $self->invnum. ': '. - $transaction->result_code. ": ". $transaction->error_message; - - if ( $conf->exists('emaildecline') - && grep { $_ ne 'POST' } $cust_main->invoicing_list - ) { - my @templ = $conf->config('declinetemplate'); - my $template = new Text::Template ( - TYPE => 'ARRAY', - SOURCE => [ map "$_\n", @templ ], - ) or return "($perror) can't create template: $Text::Template::ERROR"; - $template->compile() - or return "($perror) can't compile template: $Text::Template::ERROR"; - - my $templ_hash = { error => $transaction->error_message }; - - #false laziness w/FS::cust_pay::delete & fs_signup_server && ::send - $ENV{MAILADDRESS} = $invoice_from; - my $header = new Mail::Header ( [ - "From: $invoice_from", - "To: ". join(', ', grep { $_ ne 'POST' } $cust_main->invoicing_list ), - "Sender: $invoice_from", - "Reply-To: $invoice_from", - "Date: ". time2str("%a, %d %b %Y %X %z", time), - "Subject: Your credit card could not be processed", - ] ); - my $message = new Mail::Internet ( - 'Header' => $header, - 'Body' => [ $template->fill_in(HASH => $templ_hash) ], - ); - $!=0; - $message->smtpsend( Host => $smtpmachine ) - or $message->smtpsend( Host => $smtpmachine, Debug => 1 ) - or return "($perror) (customer # ". $self->custnum. - ") can't send card decline email to ". - join(', ', grep { $_ ne 'POST' } $cust_main->invoicing_list ). - " via server $smtpmachine with SMTP: $!"; - } - - return $perror; - } - -} - -=item realtime_card_cybercash - -Attempts to pay this invoice with the CyberCash CashRegister realtime gateway. - -=cut - -sub realtime_card_cybercash { - my $self = shift; - my $cust_main = $self->cust_main; - my $amount = $self->owed; - - return "CyberCash CashRegister real-time card processing not enabled!" - unless $processor eq 'cybercash3.2'; - - my $address = $cust_main->address1; - $address .= ", ". $cust_main->address2 if $cust_main->address2; - - #fix exp. date - #$cust_main->paydate =~ /^(\d+)\/\d*(\d{2})$/; - $cust_main->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/; - my $exp = "$2/$1"; - - # - - my $paybatch = $self->invnum. - '-' . time2str("%y%m%d%H%M%S", time); - - my $payname = $cust_main->payname || - $cust_main->getfield('first').' '.$cust_main->getfield('last'); - - my $country = $cust_main->country eq 'US' ? 'USA' : $cust_main->country; - - my @full_xaction = ( $xaction, - 'Order-ID' => $paybatch, - 'Amount' => "usd $amount", - 'Card-Number' => $cust_main->getfield('payinfo'), - 'Card-Name' => $payname, - 'Card-Address' => $address, - 'Card-City' => $cust_main->getfield('city'), - 'Card-State' => $cust_main->getfield('state'), - 'Card-Zip' => $cust_main->getfield('zip'), - 'Card-Country' => $country, - 'Card-Exp' => $exp, + $cust_main->realtime_bop($method, $amount, + 'description' => $description, + 'invnum' => $self->invnum, ); - my %result; - %result = &CCMckDirectLib3_2::SendCC2_1Server(@full_xaction); - - if ( $result{'MStatus'} eq 'success' ) { #cybercash smps v.2 or 3 - my $cust_pay = new FS::cust_pay ( { - 'invnum' => $self->invnum, - 'paid' => $amount, - '_date' => '', - 'payby' => 'CARD', - 'payinfo' => $cust_main->payinfo, - 'paybatch' => "$processor:$paybatch", - } ); - my $error = $cust_pay->insert; - if ( $error ) { - # gah, even with transactions. - my $e = 'WARNING: Card debited but database not updated - '. - 'error applying payment, invnum #' . $self->invnum. - " (CyberCash Order-ID $paybatch): $error"; - warn $e; - return $e; - } else { - return ''; - } -# } elsif ( $result{'Mstatus'} ne 'failure-bad-money' -# || $options{'report_badcard'} -# ) { - } else { - return 'Cybercash error, invnum #' . - $self->invnum. ':'. $result{'MErrMsg'}; - } - } =item batch_card @@ -722,7 +643,6 @@ sub batch_card { 'state' => $cust_main->getfield('state'), 'zip' => $cust_main->getfield('zip'), 'country' => $cust_main->getfield('country'), - 'trancode' => 77, 'cardnum' => $cust_main->getfield('payinfo'), 'exp' => $cust_main->getfield('paydate'), 'payname' => $cust_main->getfield('payname'), @@ -734,7 +654,7 @@ sub batch_card { ''; } -=item print_text [TIME]; +=item print_text [ TIME [ , TEMPLATE ] ] Returns an text invoice, as a list of lines. @@ -752,7 +672,7 @@ sub print_text { # my $invnum = $self->invnum; my $cust_main = qsearchs('cust_main', { 'custnum', $self->custnum } ); $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') ) - unless $cust_main->payname; + unless $cust_main->payname && $cust_main->payby ne 'CHEK'; my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits @@ -779,33 +699,52 @@ sub print_text { } #new charges - foreach ( $self->cust_bill_pkg ) { - - if ( $_->pkgnum ) { - - my($cust_pkg)=qsearchs('cust_pkg', { 'pkgnum', $_->pkgnum } ); - my($part_pkg)=qsearchs('part_pkg',{'pkgpart'=>$cust_pkg->pkgpart}); - my($pkg)=$part_pkg->pkg; - - if ( $_->setup != 0 ) { - push @buf, [ "$pkg Setup", $money_char. sprintf("%10.2f",$_->setup) ]; + foreach my $cust_bill_pkg ( + ( grep { $_->pkgnum } $self->cust_bill_pkg ), #packages first + ( grep { ! $_->pkgnum } $self->cust_bill_pkg ), #then taxes + ) { + + if ( $cust_bill_pkg->pkgnum ) { + + my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } ); + my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } ); + my $pkg = $part_pkg->pkg; + + if ( $cust_bill_pkg->setup != 0 ) { + my $description = $pkg; + $description .= ' Setup' if $cust_bill_pkg->recur != 0; + push @buf, [ $description, + $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ]; push @buf, map { [ " ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels; } - if ( $_->recur != 0 ) { + if ( $cust_bill_pkg->recur != 0 ) { push @buf, [ - "$pkg (" . time2str("%x",$_->sdate) . " - " . - time2str("%x",$_->edate) . ")", - $money_char. sprintf("%10.2f",$_->recur) + "$pkg (" . time2str("%x", $cust_bill_pkg->sdate) . " - " . + time2str("%x", $cust_bill_pkg->edate) . ")", + $money_char. sprintf("%10.2f", $cust_bill_pkg->recur) ]; push @buf, map { [ " ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels; } - } else { #pkgnum Tax - push @buf,["Tax", $money_char. sprintf("%10.2f",$_->setup) ] - if $_->setup != 0; + push @buf, map { [ " $_", '' ] } $cust_bill_pkg->details; + + } else { #pkgnum tax or one-shot line item + my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc') + ? ( $cust_bill_pkg->itemdesc || 'Tax' ) + : 'Tax'; + if ( $cust_bill_pkg->setup != 0 ) { + push @buf, [ $itemdesc, + $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ]; + } + if ( $cust_bill_pkg->recur != 0 ) { + push @buf, [ "$itemdesc (". time2str("%x", $cust_bill_pkg->sdate). " - " + . time2str("%x", $cust_bill_pkg->edate). ")", + $money_char. sprintf("%10.2f", $cust_bill_pkg->recur) + ]; + } } } @@ -852,8 +791,10 @@ sub print_text { } #balance due + my $balance_due_msg = $self->balance_due_msg; + push @buf,['','-----------']; - push @buf,['Balance Due', $money_char. + push @buf,[$balance_due_msg, $money_char. sprintf("%10.2f", $balance_due ) ]; #create the template @@ -863,9 +804,9 @@ sub print_text { or die "cannot load config file $templatefile"; $invoice_lines = 0; my $wasfunc = 0; - foreach ( grep /invoice_lines\(\d+\)/, @invoice_template ) { #kludgy - /invoice_lines\((\d+)\)/; - $invoice_lines += $1; + foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy + /invoice_lines\((\d*)\)/; + $invoice_lines += $1 || scalar(@buf); $wasfunc=1; } die "no invoice_lines() functions in template?" unless $wasfunc; @@ -878,11 +819,12 @@ sub print_text { #setup template variables package FS::cust_bill::_template; #! - use vars qw( $invnum $date $page $total_pages @address $overdue @buf ); + use vars qw( $invnum $date $page $total_pages @address $overdue @buf $agent ); $invnum = $self->invnum; $date = $self->_date; $page = 1; + $agent = $self->cust_main->agent->agent; if ( $FS::cust_bill::invoice_lines ) { $total_pages = @@ -923,16 +865,14 @@ sub print_text { # ); #and subroutine for the template - sub FS::cust_bill::_template::invoice_lines { - my $lines = shift or return @buf; + my $lines = shift || scalar(@buf); map { scalar(@buf) ? shift @buf : [ '', '' ]; } ( 1 .. $lines ); } - #and fill it in $FS::cust_bill::_template::page = 1; my $lines; @@ -948,11 +888,506 @@ sub print_text { } -=back +=item print_latex [ TIME [ , TEMPLATE ] ] + +Internal method - returns a filename of a filled-in LaTeX template for this +invoice (Note: add ".tex" to get the actual filename). + +See print_ps and print_pdf for methods that return PostScript and PDF output. + +TIME an optional value used to control the printing of overdue messages. The +default is now. It isn't the date of the invoice; that's the `_date' field. +It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see +L<Time::Local> and L<Date::Parse> for conversion functions. + +=cut + +#still some false laziness w/print_text +sub print_latex { + + my( $self, $today, $template ) = @_; + $today ||= time; + +# my $invnum = $self->invnum; + my $cust_main = $self->cust_main; + $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') ) + unless $cust_main->payname && $cust_main->payby ne 'CHEK'; + + my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance +# my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits + #my $balance_due = $self->owed + $pr_total - $cr_total; + my $balance_due = $self->owed + $pr_total; -=head1 VERSION + #my @collect = (); + #my($description,$amount); + @buf = (); + + #create the template + my $templatefile = 'invoice_latex'; + $templatefile .= "_$template" if $template; + my @invoice_template = $conf->config($templatefile) + or die "cannot load config file $templatefile"; + + my %invoice_data = ( + 'invnum' => $self->invnum, + 'date' => time2str('%b %o, %Y', $self->_date), + 'agent' => _latex_escape($cust_main->agent->agent), + 'payname' => _latex_escape($cust_main->payname), + 'company' => _latex_escape($cust_main->company), + 'address1' => _latex_escape($cust_main->address1), + 'address2' => _latex_escape($cust_main->address2), + 'city' => _latex_escape($cust_main->city), + 'state' => _latex_escape($cust_main->state), + 'zip' => _latex_escape($cust_main->zip), + 'country' => _latex_escape($cust_main->country), + 'footer' => join("\n", $conf->config('invoice_latexfooter') ), + 'smallfooter' => join("\n", $conf->config('invoice_latexsmallfooter') ), + 'quantity' => 1, + 'terms' => $conf->config('invoice_default_terms') || 'Payable upon receipt', + #'notes' => join("\n", $conf->config('invoice_latexnotes') ), + ); + + my $countrydefault = $conf->config('countrydefault') || 'US'; + $invoice_data{'country'} = '' if $invoice_data{'country'} eq $countrydefault; + + #do variable substitutions in notes + $invoice_data{'notes'} = + join("\n", + map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } + $conf->config('invoice_latexnotes') + ); + + $invoice_data{'footer'} =~ s/\n+$//; + $invoice_data{'smallfooter'} =~ s/\n+$//; + $invoice_data{'notes'} =~ s/\n+$//; + + $invoice_data{'po_line'} = + ( $cust_main->payby eq 'BILL' && $cust_main->payinfo ) + ? _latex_escape("Purchase Order #". $cust_main->payinfo) + : '~'; + + my @line_item = (); + my @total_item = (); + my @filled_in = (); + while ( @invoice_template ) { + my $line = shift @invoice_template; + + if ( $line =~ /^%%Detail\s*$/ ) { + + while ( ( my $line_item_line = shift @invoice_template ) + !~ /^%%EndDetail\s*$/ ) { + push @line_item, $line_item_line; + } + foreach my $line_item ( $self->_items ) { + #foreach my $line_item ( $self->_items_pkg ) { + $invoice_data{'ref'} = $line_item->{'pkgnum'}; + $invoice_data{'description'} = _latex_escape($line_item->{'description'}); + if ( exists $line_item->{'ext_description'} ) { + $invoice_data{'description'} .= + "\\tabularnewline\n~~". + join("\\tabularnewline\n~~", map { _latex_escape($_) } @{$line_item->{'ext_description'}} ); + } + $invoice_data{'amount'} = $line_item->{'amount'}; + $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A'; + push @filled_in, + map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item; + } + + } elsif ( $line =~ /^%%TotalDetails\s*$/ ) { + + while ( ( my $total_item_line = shift @invoice_template ) + !~ /^%%EndTotalDetails\s*$/ ) { + push @total_item, $total_item_line; + } + + my @total_fill = (); + + my $taxtotal = 0; + foreach my $tax ( $self->_items_tax ) { + $invoice_data{'total_item'} = _latex_escape($tax->{'description'}); + $taxtotal += ( $invoice_data{'total_amount'} = $tax->{'amount'} ); + push @total_fill, + map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } + @total_item; + } + + if ( $taxtotal ) { + $invoice_data{'total_item'} = 'Sub-total'; + $invoice_data{'total_amount'} = + '\dollar '. sprintf('%.2f', $self->charged - $taxtotal ); + unshift @total_fill, + map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } + @total_item; + } + + $invoice_data{'total_item'} = '\textbf{Total}'; + $invoice_data{'total_amount'} = + '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}'; + push @total_fill, + map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } + @total_item; + + #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments + + # credits + foreach my $credit ( $self->_items_credits ) { + $invoice_data{'total_item'} = _latex_escape($credit->{'description'}); + #$credittotal + $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'}; + push @total_fill, + map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } + @total_item; + } + + # payments + foreach my $payment ( $self->_items_payments ) { + $invoice_data{'total_item'} = _latex_escape($payment->{'description'}); + #$paymenttotal + $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'}; + push @total_fill, + map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } + @total_item; + } + + $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}'; + $invoice_data{'total_amount'} = + '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}'; + push @total_fill, + map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } + @total_item; + + push @filled_in, @total_fill; + + } else { + #$line =~ s/\$(\w+)/$invoice_data{$1}/eg; + $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg; + push @filled_in, $line; + } + + } + + sub nounder { + my $var = $1; + $var =~ s/_/\-/g; + $var; + } + + my $dir = '/tmp'; #! /usr/local/etc/freeside/invoices.datasrc/ + my $unique = int(rand(2**31)); #UGH... use File::Temp or something + + chdir($dir); + my $file = $self->invnum. ".$unique"; + + open(TEX,">$file.tex") or die "can't open $file.tex: $!\n"; + print TEX join("\n", @filled_in ), "\n"; + close TEX; + + return $file; + +} + +=item print_ps [ TIME [ , TEMPLATE ] ] + +Returns an postscript invoice, as a scalar. + +TIME an optional value used to control the printing of overdue messages. The +default is now. It isn't the date of the invoice; that's the `_date' field. +It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see +L<Time::Local> and L<Date::Parse> for conversion functions. + +=cut + +sub print_ps { + my $self = shift; + + my $file = $self->print_latex(@_); + + #error checking!! + system('pslatex', "$file.tex"); + system('pslatex', "$file.tex"); + system('dvips', '-q', '-t', 'letter', "$file.dvi", '-o', "$file.ps" ); + + open(POSTSCRIPT, "<$file.ps") + or die "can't open $file.ps (probable error in LaTeX template): $!\n"; + + unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex"); + + my $ps = ''; + while (<POSTSCRIPT>) { + $ps .= $_; + } + + close POSTSCRIPT; + + return $ps; + +} + +=item print_pdf [ TIME [ , TEMPLATE ] ] + +Returns an PDF invoice, as a scalar. + +TIME an optional value used to control the printing of overdue messages. The +default is now. It isn't the date of the invoice; that's the `_date' field. +It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see +L<Time::Local> and L<Date::Parse> for conversion functions. + +=cut + +sub print_pdf { + my $self = shift; + + my $file = $self->print_latex(@_); + + #system('pdflatex', "$file.tex"); + #system('pdflatex', "$file.tex"); + #! LaTeX Error: Unknown graphics extension: .eps. + + #error checking!! + system('pslatex', "$file.tex"); + system('pslatex', "$file.tex"); + + #system('dvipdf', "$file.dvi", "$file.pdf" ); + system("dvips -q -t letter -f $file.dvi | gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$file.pdf -c save pop -"); + + open(PDF, "<$file.pdf") + or die "can't open $file.pdf (probably error in LaTeX tempalte: $!\n"; -$Id: cust_bill.pm,v 1.38 2002-06-26 02:37:48 ivan Exp $ + unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex"); + + my $pdf = ''; + while (<PDF>) { + $pdf .= $_; + } + + close PDF; + + return $pdf; + +} + +# quick subroutine for print_latex +# +# There are ten characters that LaTeX treats as special characters, which +# means that they do not simply typeset themselves: +# # $ % & ~ _ ^ \ { } +# +# TeX ignores blanks following an escaped character; if you want a blank (as +# in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ..."). + +sub _latex_escape { + my $value = shift; + $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( length($2) ? "\\$2" : '' )/ge; + $value; +} + +#utility methods for print_* + +sub balance_due_msg { + my $self = shift; + my $msg = 'Balance Due'; + return $msg unless $conf->exists('invoice_default_terms'); + if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) { + $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) ); + } elsif ( $conf->config('invoice_default_terms') ) { + $msg .= ' - '. $conf->config('invoice_default_terms'); + } + $msg; +} + +sub _items { + my $self = shift; + my @display = scalar(@_) + ? @_ + : qw( _items_previous _items_pkg ); + #: qw( _items_pkg ); + #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments ); + my @b = (); + foreach my $display ( @display ) { + push @b, $self->$display(@_); + } + @b; +} + +sub _items_previous { + my $self = shift; + my $cust_main = $self->cust_main; + my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance + my @b = (); + foreach ( @pr_cust_bill ) { + push @b, { + 'description' => 'Previous Balance, Invoice #'. $_->invnum. + ' ('. time2str('%x',$_->_date). ')', + #'pkgpart' => 'N/A', + 'pkgnum' => 'N/A', + 'amount' => sprintf("%10.2f", $_->owed), + }; + } + @b; + + #{ + # 'description' => 'Previous Balance', + # #'pkgpart' => 'N/A', + # 'pkgnum' => 'N/A', + # 'amount' => sprintf("%10.2f", $pr_total ), + # 'ext_description' => [ map { + # "Invoice ". $_->invnum. + # " (". time2str("%x",$_->_date). ") ". + # sprintf("%10.2f", $_->owed) + # } @pr_cust_bill ], + + #}; +} + +sub _items_pkg { + my $self = shift; + my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg; + $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_); +} + +sub _items_tax { + my $self = shift; + my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg; + $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_); +} + +sub _items_cust_bill_pkg { + my $self = shift; + my $cust_bill_pkg = shift; + + my @b = (); + foreach my $cust_bill_pkg ( @$cust_bill_pkg ) { + + if ( $cust_bill_pkg->pkgnum ) { + + my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } ); + my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } ); + my $pkg = $part_pkg->pkg; + + my %labels; + #tie %labels, 'Tie::IxHash'; + push @{ $labels{$_->[0]} }, $_->[1] foreach $cust_pkg->labels; + my @ext_description; + foreach my $label ( keys %labels ) { + my @values = @{ $labels{$label} }; + my $num = scalar(@values); + if ( $num > 5 ) { + push @ext_description, "$label ($num)"; + } else { + push @ext_description, map { "$label: $_" } @values; + } + } + + if ( $cust_bill_pkg->setup != 0 ) { + my $description = $pkg; + $description .= ' Setup' if $cust_bill_pkg->recur != 0; + my @d = @ext_description; + push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0; + push @b, { + 'description' => $description, + #'pkgpart' => $part_pkg->pkgpart, + 'pkgnum' => $cust_pkg->pkgnum, + 'amount' => sprintf("%10.2f", $cust_bill_pkg->setup), + 'ext_description' => \@d, + }; + } + + if ( $cust_bill_pkg->recur != 0 ) { + push @b, { + 'description' => "$pkg (" . + time2str('%x', $cust_bill_pkg->sdate). ' - '. + time2str('%x', $cust_bill_pkg->edate). ')', + #'pkgpart' => $part_pkg->pkgpart, + 'pkgnum' => $cust_pkg->pkgnum, + 'amount' => sprintf("%10.2f", $cust_bill_pkg->recur), + 'ext_description' => [ @ext_description, + $cust_bill_pkg->details, + ], + }; + } + + } else { #pkgnum tax or one-shot line item (??) + + my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc') + ? ( $cust_bill_pkg->itemdesc || 'Tax' ) + : 'Tax'; + if ( $cust_bill_pkg->setup != 0 ) { + push @b, { + 'description' => $itemdesc, + 'amount' => sprintf("%10.2f", $cust_bill_pkg->setup), + }; + } + if ( $cust_bill_pkg->recur != 0 ) { + push @b, { + 'description' => "$itemdesc (". + time2str("%x", $cust_bill_pkg->sdate). ' - '. + time2str("%x", $cust_bill_pkg->edate). ')', + 'amount' => sprintf("%10.2f", $cust_bill_pkg->recur), + }; + } + + } + + } + + @b; + +} + +sub _items_credits { + my $self = shift; + + my @b; + #credits + foreach ( $self->cust_credited ) { + + #something more elaborate if $_->amount ne $_->cust_credit->credited ? + + my $reason = $_->cust_credit->reason; + #my $reason = substr($_->cust_credit->reason,0,32); + #$reason .= '...' if length($reason) < length($_->cust_credit->reason); + $reason = " ($reason) " if $reason; + push @b, { + #'description' => 'Credit ref\#'. $_->crednum. + # " (". time2str("%x",$_->cust_credit->_date) .")". + # $reason, + 'description' => 'Credit applied'. + time2str("%x",$_->cust_credit->_date). $reason, + 'amount' => sprintf("%10.2f",$_->amount), + }; + } + #foreach ( @cr_cust_credit ) { + # push @buf,[ + # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")", + # $money_char. sprintf("%10.2f",$_->credited) + # ]; + #} + + @b; + +} + +sub _items_payments { + my $self = shift; + + my @b; + #get & print payments + foreach ( $self->cust_bill_pay ) { + + #something more elaborate if $_->amount ne ->cust_pay->paid ? + + push @b, { + 'description' => "Payment received ". + time2str("%x",$_->cust_pay->_date ), + 'amount' => sprintf("%10.2f", $_->amount ) + }; + } + + @b; + +} + +=back =head1 BUGS diff --git a/FS/FS/cust_bill_event.pm b/FS/FS/cust_bill_event.pm index f631987aa..ddd676281 100644 --- a/FS/FS/cust_bill_event.pm +++ b/FS/FS/cust_bill_event.pm @@ -3,6 +3,7 @@ package FS::cust_bill_event; use strict; use vars qw( @ISA ); use FS::Record qw( qsearch qsearchs ); +use FS::cust_bill; use FS::part_bill_event; @ISA = qw(FS::Record); @@ -43,6 +44,10 @@ currently supported: =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion functions. +=item status - event status: B<done> or B<failed> + +=item statustext - additional status detail (i.e. error message) + =back =head1 METHODS @@ -111,13 +116,13 @@ sub check { || $self->ut_textn('statustext') ; - return "Unknown invnum" + return "Unknown invnum ". $self->invnum unless qsearchs( 'cust_bill' ,{ 'invnum' => $self->invnum } ); - return "Unknown eventpart" + return "Unknown eventpart ". $self->eventpart unless qsearchs( 'part_bill_event' ,{ 'eventpart' => $self->eventpart } ); - ''; #no error + $self->SUPER::check; } =item part_bill_event diff --git a/FS/FS/cust_bill_pay.pm b/FS/FS/cust_bill_pay.pm index 913704bef..c8b5525ea 100644 --- a/FS/FS/cust_bill_pay.pm +++ b/FS/FS/cust_bill_pay.pm @@ -1,13 +1,18 @@ package FS::cust_bill_pay; use strict; -use vars qw( @ISA ); +use vars qw( @ISA $conf ); use FS::Record qw( qsearch qsearchs dbh ); use FS::cust_bill; use FS::cust_pay; @ISA = qw( FS::Record ); +#ask FS::UID to run this stuff for us later +FS::UID->install_callback( sub { + $conf = new FS::Conf; +} ); + =head1 NAME FS::cust_bill_pay - Object methods for cust_bill_pay records @@ -101,7 +106,8 @@ sub insert { " greater than cust_pay.paid ". $cust_pay->paid; } - my $cust_bill = qsearchs('cust_bill', { 'invnum' => $self->invnum } ) or do { + my $cust_bill = $self->cust_bill; + unless ( $cust_bill ) { $dbh->rollback if $oldAutoCommit; return "unknown cust_bill.invnum: ". $self->invnum; }; @@ -120,6 +126,11 @@ sub insert { $dbh->commit or die $dbh->errstr if $oldAutoCommit; + if ( $conf->exists('invoice_send_receipts') ) { + my $send_error = $cust_bill->send; + warn "Error sending receipt: $send_error\n" if $send_error; + } + ''; } @@ -170,7 +181,7 @@ sub check { $self->_date(time) unless $self->_date; - ''; #no error + $self->SUPER::check; } =item cust_pay @@ -197,10 +208,6 @@ sub cust_bill { =back -=head1 VERSION - -$Id: cust_bill_pay.pm,v 1.12 2002-02-07 22:29:34 ivan Exp $ - =head1 BUGS Delete and replace methods. diff --git a/FS/FS/cust_bill_pkg.pm b/FS/FS/cust_bill_pkg.pm index 72f9ce4a9..6800707fe 100644 --- a/FS/FS/cust_bill_pkg.pm +++ b/FS/FS/cust_bill_pkg.pm @@ -2,11 +2,12 @@ package FS::cust_bill_pkg; use strict; use vars qw( @ISA ); -use FS::Record qw( qsearchs ); +use FS::Record qw( qsearch qsearchs dbdef dbh ); use FS::cust_pkg; use FS::cust_bill; +use FS::cust_bill_pkg_detail; -@ISA = qw(FS::Record ); +@ISA = qw( FS::Record ); =head1 NAME @@ -47,6 +48,8 @@ supported: =item edate - ending date of recurring fee +=item itemdesc - Line item description (currentlty used only when pkgnum is 0) + =back sdate and edate are specified as UNIX timestamps; see L<perlfunc/"time">. Also @@ -71,6 +74,51 @@ sub table { 'cust_bill_pkg'; } Adds this line item 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; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + + unless ( defined dbdef->table('cust_bill_pkg_detail') && $self->get('details') ) { + $dbh->commit or die $dbh->errstr if $oldAutoCommit; + return ''; + } + + foreach my $detail ( @{$self->get('details')} ) { + my $cust_bill_pkg_detail = new FS::cust_bill_pkg_detail { + 'pkgnum' => $self->pkgnum, + 'invnum' => $self->invnum, + 'detail' => $detail, + }; + $error = $cust_bill_pkg_detail->insert; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + } + + $dbh->commit or die $dbh->errstr if $oldAutoCommit; + ''; + +} + =item delete Currently unimplemented. I don't remove line items because there would then be @@ -111,6 +159,7 @@ sub check { || $self->ut_money('recur') || $self->ut_numbern('sdate') || $self->ut_numbern('edate') + || $self->ut_textn('itemdesc') ; return $error if $error; @@ -122,7 +171,7 @@ sub check { return "Unknown invnum" unless qsearchs( 'cust_bill' ,{ 'invnum' => $self->invnum } ); - ''; #no error + $self->SUPER::check; } =item cust_pkg @@ -136,11 +185,22 @@ sub cust_pkg { qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } ); } -=back +=item details + +Returns an array of detail information for the invoice line item. -=head1 VERSION +=cut -$Id: cust_bill_pkg.pm,v 1.3 2002-04-06 22:32:43 ivan Exp $ +sub details { + my $self = shift; + return () unless defined dbdef->table('cust_bill_pkg_detail'); + map { $_->detail } + qsearch ( 'cust_bill_pkg_detail', { 'pkgnum' => $self->pkgnum, + 'invnum' => $self->invnum, } ); + #qsearch ( 'cust_bill_pkg_detail', { 'lineitemnum' => $self->lineitemnum }); +} + +=back =head1 BUGS diff --git a/FS/FS/cust_bill_pkg_detail.pm b/FS/FS/cust_bill_pkg_detail.pm new file mode 100644 index 000000000..261aa80ea --- /dev/null +++ b/FS/FS/cust_bill_pkg_detail.pm @@ -0,0 +1,124 @@ +package FS::cust_bill_pkg_detail; + +use strict; +use vars qw( @ISA ); +use FS::Record qw( qsearch qsearchs ); + +@ISA = qw(FS::Record); + +=head1 NAME + +FS::cust_bill_pkg_detail - Object methods for cust_bill_pkg_detail records + +=head1 SYNOPSIS + + use FS::cust_bill_pkg_detail; + + $record = new FS::cust_bill_pkg_detail \%hash; + $record = new FS::cust_bill_pkg_detail { 'column' => 'value' }; + + $error = $record->insert; + + $error = $new_record->replace($old_record); + + $error = $record->delete; + + $error = $record->check; + +=head1 DESCRIPTION + +An FS::cust_bill_pkg_detail object represents additional detail information for +an invoice line item (see L<FS::cust_bill_pkg>). FS::cust_bill_pkg_detail +inherits from FS::Record. The following fields are currently supported: + +=over 4 + +=item detailnum - primary key + +=item pkgnum - + +=item invnum - + +=item detail - detail description + +=back + +=head1 METHODS + +=over 4 + +=item new HASHREF + +Creates a new line item detail. To add the line item detail to the database, +see L<"insert">. + +Note that this stores the hash reference, not a distinct copy of the hash it +points to. You can ask the object for a copy with the I<hash> method. + +=cut + +# the new method can be inherited from FS::Record, if a table method is defined + +sub table { 'cust_bill_pkg_detail'; } + +=item insert + +Adds this record to the database. If there is an error, returns the error, +otherwise returns false. + +=cut + +# the insert method can be inherited from FS::Record + +=item delete + +Delete this record from the database. + +=cut + +# the delete method can be inherited from FS::Record + +=item replace OLD_RECORD + +Replaces the OLD_RECORD with this one in the database. If there is an error, +returns the error, otherwise returns false. + +=cut + +# the replace method can be inherited from FS::Record + +=item check + +Checks all fields to make sure this is a valid line item detail. If there is +an error, returns the error, otherwise returns false. Called by the insert +and replace methods. + +=cut + +# the check method should currently be supplied - FS::Record contains some +# data checking routines + +sub check { + my $self = shift; + + $self->ut_numbern('detailnum') + || $self->ut_foreign_key('pkgnum', 'cust_pkg', 'pkgnum') + || $self->ut_foreign_key('invnum', 'cust_pkg', 'invnum') + || $self->ut_text('detail') + || $self->SUPER::check + ; + +} + +=back + +=head1 BUGS + +=head1 SEE ALSO + +L<FS::cust_bill_pkg>, L<FS::Record>, schema.html from the base documentation. + +=cut + +1; + diff --git a/FS/FS/cust_credit.pm b/FS/FS/cust_credit.pm index 284d59de2..19a54534f 100644 --- a/FS/FS/cust_credit.pm +++ b/FS/FS/cust_credit.pm @@ -2,8 +2,10 @@ package FS::cust_credit; use strict; use vars qw( @ISA $conf $unsuspendauto ); +use Date::Format; use FS::UID qw( dbh getotaker ); use FS::Record qw( qsearch qsearchs ); +use FS::Misc qw(send_email); use FS::cust_main; use FS::cust_refund; use FS::cust_credit_bill; @@ -130,7 +132,64 @@ Currently unimplemented. sub delete { my $self = shift; return "Can't delete closed credit" if $self->closed =~ /^Y/i; - $self->SUPER::delete(@_); + + 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; + + foreach my $cust_credit_bill ( $self->cust_credit_bill ) { + my $error = $cust_credit_bill->delete; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + } + + my $error = $self->SUPER::delete(@_); + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + + if ( $conf->config('deletecredits') ne '' ) { + + my $cust_main = qsearchs('cust_main',{ 'custnum' => $self->custnum }); + + my $error = send_email( + 'from' => $conf->config('invoice_from'), #??? well as good as any + 'to' => $conf->config('deletecredits'), + 'subject' => 'FREESIDE NOTIFICATION: Credit deleted', + 'body' => [ + "This is an automatic message from your Freeside installation\n", + "informing you that the following credit has been deleted:\n", + "\n", + 'crednum: '. $self->crednum. "\n", + 'custnum: '. $self->custnum. + " (". $cust_main->last. ", ". $cust_main->first. ")\n", + 'amount: $'. sprintf("%.2f", $self->amount). "\n", + 'date: '. time2str("%a %b %e %T %Y", $self->_date). "\n", + 'reason: '. $self->reason. "\n", + ], + ); + + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "can't send credit deletion notification: $error"; + } + + } + + $dbh->commit or die $dbh->errstr if $oldAutoCommit; + + ''; + } =item replace OLD_RECORD @@ -141,7 +200,10 @@ posted. =cut sub replace { - return "Can't modify credit!" + #return "Can't modify credit!" + my $self = shift; + return "Can't modify closed credit" if $self->closed =~ /^Y/i; + $self->SUPER::replace(@_); } =item check @@ -174,7 +236,7 @@ sub check { $self->otaker(getotaker); - ''; #no error + $self->SUPER::check; } =item cust_refund @@ -240,13 +302,9 @@ sub credited { =back -=head1 VERSION - -$Id: cust_credit.pm,v 1.16 2002-06-04 14:35:52 ivan Exp $ - =head1 BUGS -The delete method. +The delete method. The replace method. =head1 SEE ALSO diff --git a/FS/FS/cust_credit_bill.pm b/FS/FS/cust_credit_bill.pm index 62215419c..bd76c2e1a 100644 --- a/FS/FS/cust_credit_bill.pm +++ b/FS/FS/cust_credit_bill.pm @@ -1,7 +1,7 @@ package FS::cust_credit_bill; use strict; -use vars qw( @ISA ); +use vars qw( @ISA $conf ); use FS::UID qw( getotaker ); use FS::Record qw( qsearch qsearchs ); use FS::cust_main; @@ -11,6 +11,11 @@ use FS::cust_bill; @ISA = qw( FS::Record ); +#ask FS::UID to run this stuff for us later +FS::UID->install_callback( sub { + $conf = new FS::Conf; +} ); + =head1 NAME FS::cust_credit_bill - Object methods for cust_credit_bill records @@ -69,6 +74,21 @@ sub table { 'cust_credit_bill'; } Adds this cust_credit_bill to the database ("Posts" all or part of a credit). If there is an error, returns the error, otherwise returns false. +=cut + +sub insert { + my $self = shift; + my $error = $self->SUPER::insert(@_); + return $error if $error; + + if ( $conf->exists('invoice_send_receipts') ) { + my $send_error = $self->cust_bill->send; + warn "Error sending receipt: $send_error\n" if $send_error; + } + + ''; +} + =item delete Currently unimplemented. @@ -76,7 +96,10 @@ Currently unimplemented. =cut sub delete { - return "Can't unapply credit!" + my $self = shift; + return "Can't delete application for closed credit" + if $self->cust_credit->closed =~ /^Y/i; + $self->SUPER::delete(@_); } =item replace OLD_RECORD @@ -127,7 +150,7 @@ sub check { return "Cannot apply more than remaining value of invoice" unless $self->amount <= $cust_bill->owed; - ''; #no error + $self->SUPER::check; } =item sub cust_credit @@ -141,11 +164,18 @@ sub cust_credit { qsearchs( 'cust_credit', { 'crednum' => $self->crednum } ); } -=back +=item cust_bill + +Returns the invoice (see L<FS::cust_bill>) + +=cut -=head1 VERSION +sub cust_bill { + my $self = shift; + qsearchs( 'cust_bill', { 'invnum' => $self->invnum } ); +} -$Id: cust_credit_bill.pm,v 1.7 2002-01-24 16:58:47 ivan Exp $ +=back =head1 BUGS diff --git a/FS/FS/cust_credit_refund.pm b/FS/FS/cust_credit_refund.pm index cc3b32cdb..d0deae2f3 100644 --- a/FS/FS/cust_credit_refund.pm +++ b/FS/FS/cust_credit_refund.pm @@ -156,7 +156,7 @@ sub check { return "unknown cust_credit.crednum: ". $self->crednum unless qsearchs( 'cust_credit', { 'crednum' => $self->crednum } ); - ''; #no error + $self->SUPER::check; } =item cust_refund @@ -185,7 +185,7 @@ sub cust_credit { =head1 VERSION -$Id: cust_credit_refund.pm,v 1.9 2002-01-26 01:52:31 ivan Exp $ +$Id: cust_credit_refund.pm,v 1.10 2003-08-05 00:20:41 khoff Exp $ =head1 BUGS diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm index eb468d981..6ca32871d 100644 --- a/FS/FS/cust_main.pm +++ b/FS/FS/cust_main.pm @@ -2,19 +2,27 @@ package FS::cust_main; use strict; use vars qw( @ISA $conf $Debug $import ); +use vars qw( $realtime_bop_decline_quiet ); #ugh use Safe; use Carp; -use Time::Local; +BEGIN { + eval "use Time::Local;"; + die "Time::Local minimum version 1.05 required with Perl versions before 5.6" + if $] < 5.006 && !defined($Time::Local::VERSION); + eval "use Time::Local qw(timelocal timelocal_nocheck);"; +} use Date::Format; #use Date::Manip; use Business::CreditCard; use FS::UID qw( getotaker dbh ); use FS::Record qw( qsearchs qsearch dbdef ); +use FS::Misc qw( send_email ); use FS::cust_pkg; use FS::cust_bill; use FS::cust_bill_pkg; use FS::cust_pay; use FS::cust_credit; +use FS::cust_refund; use FS::part_referral; use FS::cust_main_county; use FS::agent; @@ -32,13 +40,16 @@ use FS::Msgcat qw(gettext); @ISA = qw( FS::Record ); +$realtime_bop_decline_quiet = 0; + $Debug = 0; #$Debug = 1; $import = 0; #ask FS::UID to run this stuff for us later -$FS::UID::callback{'FS::cust_main'} = sub { +#$FS::UID::callback{'FS::cust_main'} = sub { +install_callback FS::UID sub { $conf = new FS::Conf; #yes, need it for stuff below (prolly should be cached) }; @@ -158,10 +169,12 @@ FS::Record. The following fields are currently supported: =item ship_fax - phone (optional) -=item payby - `CARD' (credit cards), `BILL' (billing), `COMP' (free), or `PREPAY' (special billing type: applies a credit - see L<FS::prepay_credit> and sets billing type to BILL) +=item payby - I<CARD> (credit card - automatic), I<DCRD> (credit card - on-demand), I<CHEK> (electronic check - automatic), I<DCHK> (electronic check - on-demand), I<LECB> (Phone bill billing), I<BILL> (billing), I<COMP> (free), or I<PREPAY> (special billing type: applies a credit - see L<FS::prepay_credit> and sets billing type to I<BILL>) =item payinfo - card number, P.O., comp issuer (4-8 lowercase alphanumerics; think username) or prepayment identifier (see L<FS::prepay_credit>) +=item paycvv - Card Verification Value, "CVV2" (also known as CVC2 or CID), the 3 or 4 digit number on the back (or front, for American Express) of the credit card + =item paydate - expiration date, mm/yyyy, m/yyyy, mm/yy or m/yy =item payname - name on card or billing name @@ -172,6 +185,8 @@ FS::Record. The following fields are currently supported: =item comments - comments (optional) +=item referral_custnum - referring customer number + =back =head1 METHODS @@ -189,7 +204,7 @@ points to. You can ask the object for a copy with the I<hash> method. sub table { 'cust_main'; } -=item insert [ CUST_PKG_HASHREF [ , INVOICING_LIST_ARYREF ] ] +=item insert [ CUST_PKG_HASHREF [ , INVOICING_LIST_ARYREF ] [ , OPTION => VALUE ... ] ] Adds this customer to the database. If there is an error, returns the error, otherwise returns false. @@ -217,12 +232,18 @@ invoicing_list destination to the newly-created svc_acct. Here's an example: $cust_main->insert( {}, [ $email, 'POST' ] ); +Currently available options are: I<noexport> + +If I<noexport> is set true, no provisioning jobs (exports) are scheduled. +(You can schedule them later with the B<reexport> method.) + =cut sub insert { my $self = shift; my $cust_pkgs = @_ ? shift : {}; my $invoicing_list = @_ ? shift : ''; + my %options = @_; local $SIG{HUP} = 'IGNORE'; local $SIG{INT} = 'IGNORE'; @@ -274,26 +295,11 @@ sub insert { } # packages - foreach my $cust_pkg ( keys %$cust_pkgs ) { - $cust_pkg->custnum( $self->custnum ); - $error = $cust_pkg->insert; - if ( $error ) { - $dbh->rollback if $oldAutoCommit; - return "inserting cust_pkg (transaction rolled back): $error"; - } - foreach my $svc_something ( @{$cust_pkgs->{$cust_pkg}} ) { - $svc_something->pkgnum( $cust_pkg->pkgnum ); - if ( $seconds && $svc_something->isa('FS::svc_acct') ) { - $svc_something->seconds( $svc_something->seconds + $seconds ); - $seconds = 0; - } - $error = $svc_something->insert; - if ( $error ) { - $dbh->rollback if $oldAutoCommit; - #return "inserting svc_ (transaction rolled back): $error"; - return $error; - } - } + #local $FS::svc_Common::noexport_hack = 1 if $options{'noexport'}; + $error = $self->order_pkgs($cust_pkgs, \$seconds, %options); + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; } if ( $seconds ) { @@ -313,23 +319,115 @@ sub insert { } } - #false laziness with sub replace - my $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' }; - $error = $queue->insert($self->getfield('last'), $self->company); + $error = $self->queue_fuzzyfiles_update; if ( $error ) { $dbh->rollback if $oldAutoCommit; - return "queueing job (transaction rolled back): $error"; + return "updating fuzzy search cache: $error"; } - if ( defined $self->dbdef_table->column('ship_last') && $self->ship_last ) { - $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' }; - $error = $queue->insert($self->getfield('last'), $self->company); + $dbh->commit or die $dbh->errstr if $oldAutoCommit; + ''; + +} + +=item order_pkgs HASHREF, [ , OPTION => VALUE ... ] ] + +Like the insert method on an existing record, this method orders a package +and included services atomicaly. Pass a Tie::RefHash data structure to this +method containing FS::cust_pkg and FS::svc_I<tablename> objects. There should +be a better explanation of this, but until then, here's an example: + + use Tie::RefHash; + tie %hash, 'Tie::RefHash'; #this part is important + %hash = ( + $cust_pkg => [ $svc_acct ], + ... + ); + $cust_main->order_pkgs( \%hash, 'noexport'=>1 ); + +Currently available options are: I<noexport> + +If I<noexport> is set true, no provisioning jobs (exports) are scheduled. +(You can schedule them later with the B<reexport> method for each +cust_pkg object. Using the B<reexport> method on the cust_main object is not +recommended, as existing services will also be reexported.) + +=cut + +sub order_pkgs { + my $self = shift; + my $cust_pkgs = shift; + my $seconds = shift; + my %options = @_; + + 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; + + local $FS::svc_Common::noexport_hack = 1 if $options{'noexport'}; + + foreach my $cust_pkg ( keys %$cust_pkgs ) { + $cust_pkg->custnum( $self->custnum ); + my $error = $cust_pkg->insert; if ( $error ) { $dbh->rollback if $oldAutoCommit; - return "queueing job (transaction rolled back): $error"; + return "inserting cust_pkg (transaction rolled back): $error"; + } + foreach my $svc_something ( @{$cust_pkgs->{$cust_pkg}} ) { + $svc_something->pkgnum( $cust_pkg->pkgnum ); + if ( $seconds && $$seconds && $svc_something->isa('FS::svc_acct') ) { + $svc_something->seconds( $svc_something->seconds + $$seconds ); + $$seconds = 0; + } + $error = $svc_something->insert; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + #return "inserting svc_ (transaction rolled back): $error"; + return $error; + } + } + } + + $dbh->commit or die $dbh->errstr if $oldAutoCommit; + ''; #no error +} + +=item reexport + +Re-schedules all exports by calling the B<reexport> method of all associated +packages (see L<FS::cust_pkg>). If there is an error, returns the error; +otherwise returns false. + +=cut + +sub reexport { + 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; + + foreach my $cust_pkg ( $self->ncancelled_pkgs ) { + my $error = $cust_pkg->reexport; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; } } - #eslaf $dbh->commit or die $dbh->errstr if $oldAutoCommit; ''; @@ -343,7 +441,7 @@ returns false. This will completely remove all traces of the customer record. This is not what you want when a customer cancels service; for that, cancel all of the -customer's packages (see L<FS::cust_pkg/cancel>). +customer's packages (see L</cancel>). If the customer has any uncancelled packages, you need to pass a new (valid) customer number for those packages to be transferred to. Cancelled packages @@ -370,19 +468,19 @@ sub delete { local $FS::UID::AutoCommit = 0; my $dbh = dbh; - if ( qsearch( 'cust_bill', { 'custnum' => $self->custnum } ) ) { + if ( $self->cust_bill ) { $dbh->rollback if $oldAutoCommit; return "Can't delete a customer with invoices"; } - if ( qsearch( 'cust_credit', { 'custnum' => $self->custnum } ) ) { + if ( $self->cust_credit ) { $dbh->rollback if $oldAutoCommit; return "Can't delete a customer with credits"; } - if ( qsearch( 'cust_pay', { 'custnum' => $self->custnum } ) ) { + if ( $self->cust_pay ) { $dbh->rollback if $oldAutoCommit; return "Can't delete a customer with payments"; } - if ( qsearch( 'cust_refund', { 'custnum' => $self->custnum } ) ) { + if ( $self->cust_refund ) { $dbh->rollback if $oldAutoCommit; return "Can't delete a customer with refunds"; } @@ -461,6 +559,12 @@ sub replace { local $SIG{TSTP} = 'IGNORE'; local $SIG{PIPE} = 'IGNORE'; + if ( $self->payby eq 'COMP' && $self->payby ne $old->payby + && $conf->config('users-allow_comp') ) { + return "You are not permitted to create complimentary accounts." + unless grep { $_ eq getotaker } $conf->config('users-allow_comp'); + } + my $oldAutoCommit = $FS::UID::AutoCommit; local $FS::UID::AutoCommit = 0; my $dbh = dbh; @@ -482,35 +586,49 @@ sub replace { $self->invoicing_list( $invoicing_list ); } - if ( $self->payby eq 'CARD' && + if ( $self->payby =~ /^(CARD|CHEK|LECB)$/ && grep { $self->get($_) ne $old->get($_) } qw(payinfo paydate payname) ) { - # card info has changed, want to retry realtime_card invoice events - #false laziness w/collect - foreach my $cust_bill_event ( - grep { - #$_->part_bill_event->plan eq 'realtime-card' - $_->part_bill_event->eventcode eq '$cust_bill->realtime_card();' - && $_->status eq 'done' - && $_->statustext - } - map { $_->cust_bill_event } - grep { $_->cust_bill_event } - $self->open_cust_bill - - ) { - my $error = $cust_bill_event->retry; - if ( $error ) { - $dbh->rollback if $oldAutoCommit; - return "error scheduling invoice events for retry: $error"; - } + # card/check/lec info has changed, want to retry realtime_ invoice events + my $error = $self->retry_realtime; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; } - #eslaf + } + $error = $self->queue_fuzzyfiles_update; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "updating fuzzy search cache: $error"; } - #false laziness with sub insert + $dbh->commit or die $dbh->errstr if $oldAutoCommit; + ''; + +} + +=item queue_fuzzyfiles_update + +Used by insert & replace to update the fuzzy search cache + +=cut + +sub queue_fuzzyfiles_update { + 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 $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' }; - $error = $queue->insert($self->getfield('last'), $self->company); + my $error = $queue->insert($self->getfield('last'), $self->company); if ( $error ) { $dbh->rollback if $oldAutoCommit; return "queueing job (transaction rolled back): $error"; @@ -518,13 +636,12 @@ sub replace { if ( defined $self->dbdef_table->column('ship_last') && $self->ship_last ) { $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' }; - $error = $queue->insert($self->getfield('last'), $self->company); + $error = $queue->insert($self->getfield('ship_last'), $self->ship_company); if ( $error ) { $dbh->rollback if $oldAutoCommit; return "queueing job (transaction rolled back): $error"; } } - #eslaf $dbh->commit or die $dbh->errstr if $oldAutoCommit; ''; @@ -561,7 +678,7 @@ sub check { || $self->ut_numbern('referral_custnum') ; #barf. need message catalogs. i18n. etc. - $error .= "Please select a advertising source." + $error .= "Please select an advertising source." if $error =~ /^Illegal or empty \(numeric\) refnum: /; return $error if $error; @@ -588,13 +705,13 @@ sub check { # bad idea to disable, causes billing to fail because of no tax rates later # unless ( $import ) { - unless ( qsearchs('cust_main_county', { + unless ( qsearch('cust_main_county', { 'country' => $self->country, 'state' => '', } ) ) { return "Unknown state/county/country: ". $self->state. "/". $self->county. "/". $self->country - unless qsearchs('cust_main_county',{ + unless qsearch('cust_main_county',{ 'state' => $self->state, 'county' => $self->county, 'country' => $self->country, @@ -664,11 +781,11 @@ sub check { } } - $self->payby =~ /^(CARD|BILL|COMP|PREPAY)$/ + $self->payby =~ /^(CARD|DCRD|CHEK|DCHK|LECB|BILL|COMP|PREPAY)$/ or return "Illegal payby: ". $self->payby; $self->payby($1); - if ( $self->payby eq 'CARD' ) { + if ( $self->payby eq 'CARD' || $self->payby eq 'DCRD' ) { my $payinfo = $self->payinfo; $payinfo =~ s/\D//g; @@ -680,16 +797,56 @@ sub check { or return gettext('invalid_card'); # . ": ". $self->payinfo; return gettext('unknown_card_type') if cardtype($self->payinfo) eq "Unknown"; + if ( defined $self->dbdef_table->column('paycvv') ) { + if ( length($self->paycvv) ) { + if ( cardtype($self->payinfo) eq 'American Express card' ) { + $self->paycvv =~ /^(\d{4})$/ + or return "CVV2 (CID) for American Express cards is four digits."; + $self->paycvv($1); + } else { + $self->paycvv =~ /^(\d{3})$/ + or return "CVV2 (CVC2/CID) is three digits."; + $self->paycvv($1); + } + } else { + $self->paycvv(''); + } + } + + } elsif ( $self->payby eq 'CHEK' || $self->payby eq 'DCHK' ) { + + my $payinfo = $self->payinfo; + $payinfo =~ s/[^\d\@]//g; + $payinfo =~ /^(\d+)\@(\d{9})$/ or return 'invalid echeck account@aba'; + $payinfo = "$1\@$2"; + $self->payinfo($payinfo); + $self->paycvv('') if $self->dbdef_table->column('paycvv'); + + } elsif ( $self->payby eq 'LECB' ) { + + my $payinfo = $self->payinfo; + $payinfo =~ s/\D//g; + $payinfo =~ /^1?(\d{10})$/ or return 'invalid btn billing telephone number'; + $payinfo = $1; + $self->payinfo($payinfo); + $self->paycvv('') if $self->dbdef_table->column('paycvv'); } elsif ( $self->payby eq 'BILL' ) { $error = $self->ut_textn('payinfo'); return "Illegal P.O. number: ". $self->payinfo if $error; + $self->paycvv('') if $self->dbdef_table->column('paycvv'); } elsif ( $self->payby eq 'COMP' ) { + if ( !$self->custnum && $conf->config('users-allow_comp') ) { + return "You are not permitted to create complimentary accounts." + unless grep { $_ eq getotaker } $conf->config('users-allow_comp'); + } + $error = $self->ut_textn('payinfo'); return "Illegal comp account issuer: ". $self->payinfo if $error; + $self->paycvv('') if $self->dbdef_table->column('paycvv'); } elsif ( $self->payby eq 'PREPAY' ) { @@ -700,24 +857,33 @@ sub check { return "Illegal prepayment identifier: ". $self->payinfo if $error; return "Unknown prepayment identifier" unless qsearchs('prepay_credit', { 'identifier' => $self->payinfo } ); + $self->paycvv('') if $self->dbdef_table->column('paycvv'); } if ( $self->paydate eq '' || $self->paydate eq '-' ) { return "Expriation date required" - unless $self->payby eq 'BILL' || $self->payby eq 'PREPAY'; + unless $self->payby =~ /^(BILL|PREPAY|CHEK|LECB)$/; $self->paydate(''); } else { - $self->paydate =~ /^(\d{1,2})[\/\-](\d{2}(\d{2})?)$/ - or return "Illegal expiration date: ". $self->paydate; - my $y = length($2) == 4 ? $2 : "20$2"; - $self->paydate("$y-$1-01"); + my( $m, $y ); + if ( $self->paydate =~ /^(\d{1,2})[\/\-](\d{2}(\d{2})?)$/ ) { + ( $m, $y ) = ( $1, length($2) == 4 ? $2 : "20$2" ); + } elsif ( $self->paydate =~ /^(20)?(\d{2})[\/\-](\d{2})[\/\-]\d+$/ ) { + ( $m, $y ) = ( $3, "20$2" ); + } else { + return "Illegal expiration date: ". $self->paydate; + } + $self->paydate("$y-$m-01"); my($nowm,$nowy)=(localtime(time))[4,5]; $nowm++; $nowy+=1900; - return gettext('expired_card') if $y<$nowy || ( $y==$nowy && $1<$nowm ); + return gettext('expired_card') + if !$import && ( $y<$nowy || ( $y==$nowy && $1<$nowm ) ); } - if ( $self->payname eq '' && - ( ! $conf->exists('require_cardname') || $self->payby ne 'CARD' ) ) { + if ( $self->payname eq '' && $self->payby !~ /^(CHEK|DCHK)$/ && + ( ! $conf->exists('require_cardname') + || $self->payby !~ /^(CARD|DCRD)$/ ) + ) { $self->payname( $self->first. " ". $self->getfield('last') ); } else { $self->payname =~ /^([\w \,\.\-\']+)$/ @@ -728,11 +894,11 @@ sub check { $self->tax =~ /^(Y?)$/ or return "Illegal tax: ". $self->tax; $self->tax($1); - $self->otaker(getotaker); + $self->otaker(getotaker) unless $self->otaker; #warn "AFTER: \n". $self->_dump; - ''; #no error + $self->SUPER::check; } =item all_pkgs @@ -836,16 +1002,21 @@ sub suspend { grep { $_->suspend } $self->unsuspended_pkgs; } -=item cancel +=item cancel [ OPTION => VALUE ... ] Cancels all uncancelled packages (see L<FS::cust_pkg>) for this customer. + +Available options are: I<quiet> + +I<quiet> can be set true to supress email cancellation notices. + Always returns a list: an empty list on success or a list of errors. =cut sub cancel { my $self = shift; - grep { $_->cancel } $self->ncancelled_pkgs; + grep { $_ } map { $_->cancel(@_) } $self->ncancelled_pkgs; } =item agent @@ -866,15 +1037,19 @@ conjunction with the collect method. Options are passed as name-value pairs. -The only currently available option is `time', which bills the customer as if -it were that time. It is specified as a UNIX timestamp; see -L<perlfunc/"time">). Also see L<Time::Local> and L<Date::Parse> for conversion -functions. For example: +Currently available options are: + +resetup - if set true, re-charges setup fees. + +time - bills the customer as if it were that time. Specified as a UNIX +timestamp; see L<perlfunc/"time">). Also see L<Time::Local> and +L<Date::Parse> for conversion functions. For example: use Date::Parse; ... $cust_main->bill( 'time' => str2time('April 20th, 2001') ); + If there is an error, returns the error, otherwise returns false. =cut @@ -903,10 +1078,12 @@ sub bill { my( $total_setup, $total_recur ) = ( 0, 0 ); #my( $taxable_setup, $taxable_recur ) = ( 0, 0 ); my @cust_bill_pkg = (); - my $tax = 0;## + #my $tax = 0;## #my $taxable_charged = 0;## #my $charged = 0;## + my %tax; + foreach my $cust_pkg ( qsearch('cust_pkg', { 'custnum' => $self->custnum } ) ) { @@ -925,9 +1102,11 @@ sub bill { my %hash = $cust_pkg->hash; my $old_cust_pkg = new FS::cust_pkg \%hash; + my @details = (); + # bill setup my $setup = 0; - unless ( $cust_pkg->setup ) { + if ( !$cust_pkg->setup || $options{'resetup'} ) { my $setup_prog = $part_pkg->getfield('setup'); $setup_prog =~ /^(.*)$/ or do { $dbh->rollback if $oldAutoCommit; @@ -935,6 +1114,7 @@ sub bill { ": $setup_prog"; }; $setup_prog = $1; + $setup_prog = '0' if $setup_prog =~ /^\s*$/; #my $cpt = new Safe; ##$cpt->permit(); #what is necessary? @@ -946,16 +1126,16 @@ sub bill { return "Error eval-ing part_pkg->setup pkgpart ". $part_pkg->pkgpart. "(expression $setup_prog): $@"; } - $cust_pkg->setfield('setup',$time); + $cust_pkg->setfield('setup', $time) unless $cust_pkg->setup; $cust_pkg_mod_flag=1; } #bill recurring fee my $recur = 0; my $sdate; - if ( $part_pkg->getfield('freq') > 0 && + if ( $part_pkg->getfield('freq') ne '0' && ! $cust_pkg->getfield('susp') && - ( $cust_pkg->getfield('bill') || 0 ) < $time + ( $cust_pkg->getfield('bill') || 0 ) <= $time ) { my $recur_prog = $part_pkg->getfield('recur'); $recur_prog =~ /^(.*)$/ or do { @@ -964,6 +1144,7 @@ sub bill { ": $recur_prog"; }; $recur_prog = $1; + $recur_prog = '0' if $recur_prog =~ /^\s*$/; # shared with $recur_prog $sdate = $cust_pkg->bill || $cust_pkg->setup || $time; @@ -987,11 +1168,24 @@ sub bill { # only for figuring next bill date, nothing else, so, reset $sdate again # here $sdate = $cust_pkg->bill || $cust_pkg->setup || $time; - - $mon += $part_pkg->freq; - until ( $mon < 12 ) { $mon -= 12; $year++; } + $cust_pkg->last_bill($sdate) + if $cust_pkg->dbdef_table->column('last_bill'); + + if ( $part_pkg->freq =~ /^\d+$/ ) { + $mon += $part_pkg->freq; + until ( $mon < 12 ) { $mon -= 12; $year++; } + } elsif ( $part_pkg->freq =~ /^(\d+)w$/ ) { + my $weeks = $1; + $mday += $weeks * 7; + } elsif ( $part_pkg->freq =~ /^(\d+)d$/ ) { + my $days = $1; + $mday += $days; + } else { + $dbh->rollback if $oldAutoCommit; + return "unparsable frequency: ". $part_pkg->freq; + } $cust_pkg->setfield('bill', - timelocal($sec,$min,$hour,$mday,$mon,$year)); + timelocal_nocheck($sec,$min,$hour,$mday,$mon,$year)); $cust_pkg_mod_flag = 1; } @@ -999,7 +1193,6 @@ sub bill { warn "\$recur is undefined" unless defined($recur); warn "\$cust_pkg->bill is undefined" unless defined($cust_pkg->bill); - my $taxable_charged = 0; if ( $cust_pkg_mod_flag ) { $error=$cust_pkg->replace($old_cust_pkg); if ( $error ) { #just in case @@ -1008,105 +1201,133 @@ sub bill { } $setup = sprintf( "%.2f", $setup ); $recur = sprintf( "%.2f", $recur ); - if ( $setup < 0 ) { + if ( $setup < 0 && ! $conf->exists('allow_negative_charges') ) { $dbh->rollback if $oldAutoCommit; return "negative setup $setup for pkgnum ". $cust_pkg->pkgnum; } - if ( $recur < 0 ) { + if ( $recur < 0 && ! $conf->exists('allow_negative_charges') ) { $dbh->rollback if $oldAutoCommit; return "negative recur $recur for pkgnum ". $cust_pkg->pkgnum; } - if ( $setup > 0 || $recur > 0 ) { + if ( $setup != 0 || $recur != 0 ) { my $cust_bill_pkg = new FS::cust_bill_pkg ({ - 'pkgnum' => $cust_pkg->pkgnum, - 'setup' => $setup, - 'recur' => $recur, - 'sdate' => $sdate, - 'edate' => $cust_pkg->bill, + 'pkgnum' => $cust_pkg->pkgnum, + 'setup' => $setup, + 'recur' => $recur, + 'sdate' => $sdate, + 'edate' => $cust_pkg->bill, + 'details' => \@details, }); push @cust_bill_pkg, $cust_bill_pkg; $total_setup += $setup; $total_recur += $recur; - $taxable_charged += $setup - unless $part_pkg->setuptax =~ /^Y$/i; - $taxable_charged += $recur - unless $part_pkg->recurtax =~ /^Y$/i; - - unless ( $self->tax =~ /Y/i - || $self->payby eq 'COMP' - || $taxable_charged == 0 ) { - - my $cust_main_county = - qsearchs('cust_main_county',{ - 'state' => $self->state, - 'county' => $self->county, - 'country' => $self->country, - 'taxclass' => $part_pkg->taxclass, - } ) - or qsearchs('cust_main_county',{ - 'state' => $self->state, - 'county' => $self->county, - 'country' => $self->country, - 'taxclass' => '', - } ) - or do { - $dbh->rollback if $oldAutoCommit; - return - "fatal: can't find tax rate for state/county/country/taxclass ". - join('/', ( map $self->$_(), qw(state county country) ), - $part_pkg->taxclass ). "\n"; - }; - - if ( $cust_main_county->exempt_amount ) { - my ($mon,$year) = (localtime($sdate) )[4,5]; - $mon++; - my $freq = $part_pkg->freq || 1; - my $taxable_per_month = sprintf("%.2f", $taxable_charged / $freq ); - foreach my $which_month ( 1 .. $freq ) { - my %hash = ( - 'custnum' => $self->custnum, - 'taxnum' => $cust_main_county->taxnum, - 'year' => 1900+$year, - 'month' => $mon++, - ); - #until ( $mon < 12 ) { $mon -= 12; $year++; } - until ( $mon < 13 ) { $mon -= 12; $year++; } - my $cust_tax_exempt = - qsearchs('cust_tax_exempt', \%hash) - || new FS::cust_tax_exempt( { %hash, 'amount' => 0 } ); - my $remaining_exemption = sprintf("%.2f", - $cust_main_county->exempt_amount - $cust_tax_exempt->amount ); - if ( $remaining_exemption > 0 ) { - my $addl = $remaining_exemption > $taxable_per_month - ? $taxable_per_month - : $remaining_exemption; - $taxable_charged -= $addl; - my $new_cust_tax_exempt = new FS::cust_tax_exempt ( { - $cust_tax_exempt->hash, - 'amount' => sprintf("%.2f", $cust_tax_exempt->amount + $addl), - } ); - $error = $new_cust_tax_exempt->exemptnum - ? $new_cust_tax_exempt->replace($cust_tax_exempt) - : $new_cust_tax_exempt->insert; - if ( $error ) { - $dbh->rollback if $oldAutoCommit; - return "fatal: can't update cust_tax_exempt: $error"; - } - - } # if $remaining_exemption > 0 - - } #foreach $which_month - - } #if $cust_main_county->exempt_amount - - $taxable_charged = sprintf( "%.2f", $taxable_charged); - $tax += $taxable_charged * $cust_main_county->tax / 100 - - } #unless $self->tax =~ /Y/i - # || $self->payby eq 'COMP' - # || $taxable_charged == 0 - - } #if $setup > 0 || $recur > 0 + + unless ( $self->tax =~ /Y/i || $self->payby eq 'COMP' ) { + + my @taxes = qsearch( 'cust_main_county', { + 'state' => $self->state, + 'county' => $self->county, + 'country' => $self->country, + 'taxclass' => $part_pkg->taxclass, + } ); + unless ( @taxes ) { + @taxes = qsearch( 'cust_main_county', { + 'state' => $self->state, + 'county' => $self->county, + 'country' => $self->country, + 'taxclass' => '', + } ); + } + + #one more try at a whole-country tax rate + unless ( @taxes ) { + @taxes = qsearch( 'cust_main_county', { + 'state' => '', + 'county' => '', + 'country' => $self->country, + 'taxclass' => '', + } ); + } + + # maybe eliminate this entirely, along with all the 0% records + unless ( @taxes ) { + $dbh->rollback if $oldAutoCommit; + return + "fatal: can't find tax rate for state/county/country/taxclass ". + join('/', ( map $self->$_(), qw(state county country) ), + $part_pkg->taxclass ). "\n"; + } + + foreach my $tax ( @taxes ) { + + my $taxable_charged = 0; + $taxable_charged += $setup + unless $part_pkg->setuptax =~ /^Y$/i + || $tax->setuptax =~ /^Y$/i; + $taxable_charged += $recur + unless $part_pkg->recurtax =~ /^Y$/i + || $tax->recurtax =~ /^Y$/i; + next unless $taxable_charged; + + if ( $tax->exempt_amount > 0 ) { + my ($mon,$year) = (localtime($sdate) )[4,5]; + $mon++; + my $freq = $part_pkg->freq || 1; + if ( $freq !~ /(\d+)$/ ) { + $dbh->rollback if $oldAutoCommit; + return "daily/weekly package definitions not (yet?)". + " compatible with monthly tax exemptions"; + } + my $taxable_per_month = sprintf("%.2f", $taxable_charged / $freq ); + foreach my $which_month ( 1 .. $freq ) { + my %hash = ( + 'custnum' => $self->custnum, + 'taxnum' => $tax->taxnum, + 'year' => 1900+$year, + 'month' => $mon++, + ); + #until ( $mon < 12 ) { $mon -= 12; $year++; } + until ( $mon < 13 ) { $mon -= 12; $year++; } + my $cust_tax_exempt = + qsearchs('cust_tax_exempt', \%hash) + || new FS::cust_tax_exempt( { %hash, 'amount' => 0 } ); + my $remaining_exemption = sprintf("%.2f", + $tax->exempt_amount - $cust_tax_exempt->amount ); + if ( $remaining_exemption > 0 ) { + my $addl = $remaining_exemption > $taxable_per_month + ? $taxable_per_month + : $remaining_exemption; + $taxable_charged -= $addl; + my $new_cust_tax_exempt = new FS::cust_tax_exempt ( { + $cust_tax_exempt->hash, + 'amount' => + sprintf("%.2f", $cust_tax_exempt->amount + $addl), + } ); + $error = $new_cust_tax_exempt->exemptnum + ? $new_cust_tax_exempt->replace($cust_tax_exempt) + : $new_cust_tax_exempt->insert; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "fatal: can't update cust_tax_exempt: $error"; + } + + } # if $remaining_exemption > 0 + + } #foreach $which_month + + } #if $tax->exempt_amount + + $taxable_charged = sprintf( "%.2f", $taxable_charged); + + #$tax += $taxable_charged * $cust_main_county->tax / 100 + $tax{ $tax->taxname || 'Tax' } += + $taxable_charged * $tax->tax / 100 + + } #foreach my $tax ( @taxes ) + + } #unless $self->tax =~ /Y/i || $self->payby eq 'COMP' + + } #if $setup != 0 || $recur != 0 } #if $cust_pkg_mod_flag @@ -1133,20 +1354,42 @@ sub bill { # $taxable_charged * ( $cust_main_county->getfield('tax') / 100 ) # ); - $tax = sprintf("%.2f", $tax); - if ( $tax > 0 ) { - $charged = sprintf( "%.2f", $charged+$tax ); + if ( dbdef->table('cust_bill_pkg')->column('itemdesc') ) { #1.5 schema + + foreach my $taxname ( grep { $tax{$_} > 0 } keys %tax ) { + my $tax = sprintf("%.2f", $tax{$taxname} ); + $charged = sprintf( "%.2f", $charged+$tax ); + + my $cust_bill_pkg = new FS::cust_bill_pkg ({ + 'pkgnum' => 0, + 'setup' => $tax, + 'recur' => 0, + 'sdate' => '', + 'edate' => '', + 'itemdesc' => $taxname, + }); + push @cust_bill_pkg, $cust_bill_pkg; + } + + } else { #1.4 schema + + my $tax = 0; + foreach ( values %tax ) { $tax += $_ }; + $tax = sprintf("%.2f", $tax); + if ( $tax > 0 ) { + $charged = sprintf( "%.2f", $charged+$tax ); + + my $cust_bill_pkg = new FS::cust_bill_pkg ({ + 'pkgnum' => 0, + 'setup' => $tax, + 'recur' => 0, + 'sdate' => '', + 'edate' => '', + }); + push @cust_bill_pkg, $cust_bill_pkg; + } - my $cust_bill_pkg = new FS::cust_bill_pkg ({ - 'pkgnum' => 0, - 'setup' => $tax, - 'recur' => 0, - 'sdate' => '', - 'edate' => '', - }); - push @cust_bill_pkg, $cust_bill_pkg; } -# } my $cust_bill = new FS::cust_bill ( { 'custnum' => $self->custnum, @@ -1181,8 +1424,9 @@ sub bill { (Attempt to) collect money for this customer's outstanding invoices (see L<FS::cust_bill>). Usually used after the bill method. -Depending on the value of `payby', this may print an invoice (`BILL'), charge -a credit card (`CARD'), or just add any necessary (pseudo-)payment (`COMP'). +Depending on the value of `payby', this may print or email an invoice (I<BILL>, +I<DCRD>, or I<DCHK>), charge a credit card (I<CARD>), charge via electronic +check/ACH (I<CHEK>), or just add any necessary (pseudo-)payment (I<COMP>). Most actions are now triggered by invoice events; see L<FS::part_bill_event> and the invoice events web interface. @@ -1197,7 +1441,10 @@ invoice_time - Use this time when deciding when to print invoices and late notices on those invoices. The default is now. It is specified as a UNIX timestamp; see L<perlfunc/"time">). Also see L<Time::Local> and L<Date::Parse> for conversion functions. -retry_card - Retry cards even when not scheduled by invoice events. +retry - Retry card/echeck/LEC transactions even when not scheduled by invoice +events. + +retry_card - Deprecated alias for 'retry' batch_card - This option is deprecated. See the invoice events web interface to control whether cards are batched or run against a realtime gateway. @@ -1206,6 +1453,8 @@ report_badcard - This option is deprecated. force_print - This option is deprecated; see the invoice events web interface. +quiet - set true to surpress email card/ACH decline notices. + =cut sub collect { @@ -1231,46 +1480,27 @@ sub collect { return ''; } - if ( exists($options{'retry_card'}) && $options{'retry_card'} ) { - #false laziness w/replace - foreach my $cust_bill_event ( - grep { - #$_->part_bill_event->plan eq 'realtime-card' - $_->part_bill_event->eventcode eq '$cust_bill->realtime_card();' - && $_->status eq 'done' - && $_->statustext - } - map { $_->cust_bill_event } - grep { $_->cust_bill_event } - $self->open_cust_bill - ) { - my $error = $cust_bill_event->retry; - if ( $error ) { - $dbh->rollback if $oldAutoCommit; - return "error scheduling invoice events for retry: $error"; - } + if ( exists($options{'retry_card'}) ) { + carp 'retry_card option passed to collect is deprecated; use retry'; + $options{'retry'} ||= $options{'retry_card'}; + } + if ( exists($options{'retry'}) && $options{'retry'} ) { + my $error = $self->retry_realtime; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; } - #eslaf } - foreach my $cust_bill ( $self->cust_bill ) { - - #this has to be before next's - my $amount = sprintf( "%.2f", $balance < $cust_bill->owed - ? $balance - : $cust_bill->owed - ); - $balance = sprintf( "%.2f", $balance - $amount ); - - next unless $cust_bill->owed > 0; + foreach my $cust_bill ( $self->open_cust_bill ) { # don't try to charge for the same invoice if it's already in a batch #next if qsearchs( 'cust_pay_batch', { 'invnum' => $cust_bill->invnum } ); - warn "invnum ". $cust_bill->invnum. " (owed ". $cust_bill->owed. ", amount $amount, balance $balance)" if $Debug; - - next unless $amount > 0; + last if $self->balance <= 0; + warn "invnum ". $cust_bill->invnum. " (owed ". $cust_bill->owed. ")" + if $Debug; foreach my $part_bill_event ( sort { $a->seconds <=> $b->seconds @@ -1287,12 +1517,18 @@ sub collect { 'disabled' => '', } ) ) { - last unless $cust_bill->owed > 0; #don't run subsequent events if owed=0 + last if $cust_bill->owed <= 0 # don't run subsequent events if owed<=0 + || $self->balance <= 0; # or if balance<=0 warn "calling invoice event (". $part_bill_event->eventcode. ")\n" if $Debug; my $cust_main = $self; #for callback - my $error = eval $part_bill_event->eventcode; + + my $error; + { + local $realtime_bop_decline_quiet = 1 if $options{'quiet'}; + $error = eval $part_bill_event->eventcode; + } my $status = ''; my $statustext = ''; @@ -1310,7 +1546,8 @@ sub collect { my $cust_bill_event = new FS::cust_bill_event { 'invnum' => $cust_bill->invnum, 'eventpart' => $part_bill_event->eventpart, - '_date' => $invoice_time, + #'_date' => $invoice_time, + '_date' => time, 'status' => $status, 'statustext' => $statustext, }; @@ -1339,6 +1576,322 @@ sub collect { } +=item retry_realtime + +Schedules realtime credit card / electronic check / LEC billing events for +for retry. Useful if card information has changed or manual retry is desired. +The 'collect' method must be called to actually retry the transaction. + +Implementation details: For each of this customer's open invoices, changes +the status of the first "done" (with statustext error) realtime processing +event to "failed". + +=cut + +sub retry_realtime { + 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; + + foreach my $cust_bill ( + grep { $_->cust_bill_event } + $self->open_cust_bill + ) { + my @cust_bill_event = + sort { $a->part_bill_event->seconds <=> $b->part_bill_event->seconds } + grep { + #$_->part_bill_event->plan eq 'realtime-card' + $_->part_bill_event->eventcode =~ + /\$cust_bill\->realtime_(card|ach|lec)/ + && $_->status eq 'done' + && $_->statustext + } + $cust_bill->cust_bill_event; + next unless @cust_bill_event; + my $error = $cust_bill_event[0]->retry; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "error scheduling invoice event for retry: $error"; + } + + } + + $dbh->commit or die $dbh->errstr if $oldAutoCommit; + ''; + +} + +=item realtime_bop METHOD AMOUNT [ OPTION => VALUE ... ] + +Runs a realtime credit card, ACH (electronic check) or phone bill transaction +via a Business::OnlinePayment realtime gateway. See +L<http://420.am/business-onlinepayment> for supported gateways. + +Available methods are: I<CC>, I<ECHECK> and I<LEC> + +Available options are: I<description>, I<invnum>, I<quiet> + +The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>, +I<zip>, I<payinfo> and I<paydate> are also available. Any of these options, +if set, will override the value from the customer record. + +I<description> is a free-text field passed to the gateway. It defaults to +"Internet services". + +If an I<invnum> is specified, this payment (if sucessful) is applied to the +specified invoice. If you don't specify an I<invnum> you might want to +call the B<apply_payments> method. + +I<quiet> can be set true to surpress email decline notices. + +(moved from cust_bill) (probably should get realtime_{card,ach,lec} here too) + +=cut + +sub realtime_bop { + my( $self, $method, $amount, %options ) = @_; + if ( $Debug ) { + warn "$self $method $amount\n"; + warn " $_ => $options{$_}\n" foreach keys %options; + } + + $options{'description'} ||= 'Internet services'; + + #pre-requisites + die "Real-time processing not enabled\n" + unless $conf->exists('business-onlinepayment'); + eval "use Business::OnlinePayment"; + die $@ if $@; + + #overrides + $self->set( $_ => $options{$_} ) + foreach grep { exists($options{$_}) } + qw( payname address1 address2 city state zip payinfo paydate ); + + #load up config + my $bop_config = 'business-onlinepayment'; + $bop_config .= '-ach' + if $method eq 'ECHECK' && $conf->exists($bop_config. '-ach'); + my ( $processor, $login, $password, $action, @bop_options ) = + $conf->config($bop_config); + $action ||= 'normal authorization'; + pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/; + + #massage data + + my $address = $self->address1; + $address .= ", ". $self->address2 if $self->address2; + + my($payname, $payfirst, $paylast); + if ( $self->payname && $method ne 'ECHECK' ) { + $payname = $self->payname; + $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/ + or return "Illegal payname $payname"; + ($payfirst, $paylast) = ($1, $2); + } else { + $payfirst = $self->getfield('first'); + $paylast = $self->getfield('last'); + $payname = "$payfirst $paylast"; + } + + my @invoicing_list = grep { $_ ne 'POST' } $self->invoicing_list; + if ( $conf->exists('emailinvoiceauto') + || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) { + push @invoicing_list, $self->all_emails; + } + my $email = $invoicing_list[0]; + + my %content; + if ( $method eq 'CC' ) { + + $content{card_number} = $self->payinfo; + $self->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/; + $content{expiration} = "$2/$1"; + + $content{cvv2} = $self->paycvv + if defined $self->dbdef_table->column('paycvv') + && length($self->paycvv); + + $content{recurring_billing} = 'YES' + if qsearch('cust_pay', { 'custnum' => $self->custnum, + 'payby' => 'CARD', + 'payinfo' => $self->payinfo, } ); + + } elsif ( $method eq 'ECHECK' ) { + my($account_number,$routing_code) = $self->payinfo; + ( $content{account_number}, $content{routing_code} ) = + split('@', $self->payinfo); + $content{bank_name} = $self->payname; + $content{account_type} = 'CHECKING'; + $content{account_name} = $payname; + $content{customer_org} = $self->company ? 'B' : 'I'; + $content{customer_ssn} = $self->ss; + } elsif ( $method eq 'LEC' ) { + $content{phone} = $self->payinfo; + } + + #transaction(s) + + my( $action1, $action2 ) = split(/\s*\,\s*/, $action ); + + my $transaction = + new Business::OnlinePayment( $processor, @bop_options ); + $transaction->content( + 'type' => $method, + 'login' => $login, + 'password' => $password, + 'action' => $action1, + 'description' => $options{'description'}, + 'amount' => $amount, + 'invoice_number' => $options{'invnum'}, + 'customer_id' => $self->custnum, + 'last_name' => $paylast, + 'first_name' => $payfirst, + 'name' => $payname, + 'address' => $address, + 'city' => $self->city, + 'state' => $self->state, + 'zip' => $self->zip, + 'country' => $self->country, + 'referer' => 'http://cleanwhisker.420.am/', + 'email' => $email, + 'phone' => $self->daytime || $self->night, + %content, #after + ); + $transaction->submit(); + + if ( $transaction->is_success() && $action2 ) { + my $auth = $transaction->authorization; + my $ordernum = $transaction->can('order_number') + ? $transaction->order_number + : ''; + + my $capture = + new Business::OnlinePayment( $processor, @bop_options ); + + my %capture = ( + %content, + type => $method, + action => $action2, + login => $login, + password => $password, + order_number => $ordernum, + amount => $amount, + authorization => $auth, + description => $options{'description'}, + ); + + foreach my $field (qw( authorization_source_code returned_ACI transaction_identifier validation_code + transaction_sequence_num local_transaction_date + local_transaction_time AVS_result_code )) { + $capture{$field} = $transaction->$field() if $transaction->can($field); + } + + $capture->content( %capture ); + + $capture->submit(); + + unless ( $capture->is_success ) { + my $e = "Authorization sucessful but capture failed, custnum #". + $self->custnum. ': '. $capture->result_code. + ": ". $capture->error_message; + warn $e; + return $e; + } + + } + + #remove paycvv after initial transaction + #make this disable-able via a config option if anyone insists? + # (though that probably violates cardholder agreements) + if ( defined $self->dbdef_table->column('paycvv') + && length($self->paycvv) + && ! grep { $_ eq cardtype($self->payinfo) } $conf->config('cvv-save') + ) { + my $new = new FS::cust_main { $self->hash }; + $new->paycvv(''); + my $error = $new->replace($self); + if ( $error ) { + warn "error removing cvv: $error\n"; + } + } + + #result handling + if ( $transaction->is_success() ) { + + my %method2payby = ( + 'CC' => 'CARD', + 'ECHECK' => 'CHEK', + 'LEC' => 'LECB', + ); + + my $cust_pay = new FS::cust_pay ( { + 'custnum' => $self->custnum, + 'invnum' => $options{'invnum'}, + 'paid' => $amount, + '_date' => '', + 'payby' => $method2payby{$method}, + 'payinfo' => $self->payinfo, + 'paybatch' => "$processor:". $transaction->authorization, + } ); + my $error = $cust_pay->insert; + if ( $error ) { + # gah, even with transactions. + my $e = 'WARNING: Card/ACH debited but database not updated - '. + 'error applying payment, invnum #' . $self->invnum. + " ($processor): $error"; + warn $e; + return $e; + } else { + return ''; + } + + } else { + + my $perror = "$processor error: ". $transaction->error_message; + + if ( !$options{'quiet'} && !$realtime_bop_decline_quiet + && $conf->exists('emaildecline') + && grep { $_ ne 'POST' } $self->invoicing_list + && ! grep { $_ eq $transaction->error_message } + $conf->config('emaildecline-exclude') + ) { + my @templ = $conf->config('declinetemplate'); + my $template = new Text::Template ( + TYPE => 'ARRAY', + SOURCE => [ map "$_\n", @templ ], + ) or return "($perror) can't create template: $Text::Template::ERROR"; + $template->compile() + or return "($perror) can't compile template: $Text::Template::ERROR"; + + my $templ_hash = { error => $transaction->error_message }; + + my $error = send_email( + 'from' => $conf->config('invoice_from'), + 'to' => [ grep { $_ ne 'POST' } $self->invoicing_list ], + 'subject' => 'Your payment could not be processed', + 'body' => [ $template->fill_in(HASH => $templ_hash) ], + ); + + $perror .= " (also received error sending decline notification: $error)" + if $error; + + } + + return $perror; + } + +} + =item total_owed Returns the total owed for this customer on all invoices @@ -1583,7 +2136,6 @@ sub invoicing_list { } my %seen = map { $_->address => 1 } @cust_main_invoice; foreach my $address ( @{$arrayref} ) { - #unless ( grep { $address eq $_->address } @cust_main_invoice ) { next if exists $seen{$address} && $seen{$address}; $seen{$address} = 1; my $cust_main_invoice = new FS::cust_main_invoice ( { @@ -1625,24 +2177,36 @@ sub check_invoicing_list { ''; } -=item default_invoicing_list +=item set_default_invoicing_list -Sets the invoicing list to all accounts associated with this customer. +Sets the invoicing list to all accounts associated with this customer, +overwriting any previous invoicing list. =cut -sub default_invoicing_list { +sub set_default_invoicing_list { my $self = shift; - my @list = (); + $self->invoicing_list($self->all_emails); +} + +=item all_emails + +Returns the email addresses of all accounts provisioned for this customer. + +=cut + +sub all_emails { + my $self = shift; + my %list; foreach my $cust_pkg ( $self->all_pkgs ) { my @cust_svc = qsearch('cust_svc', { 'pkgnum' => $cust_pkg->pkgnum } ); my @svc_acct = map { qsearchs('svc_acct', { 'svcnum' => $_->svcnum } ) } grep { qsearchs('svc_acct', { 'svcnum' => $_->svcnum } ) } @cust_svc; - push @list, map { $_->email } @svc_acct; + $list{$_}=1 foreach map { $_->email } @svc_acct; } - $self->invoicing_list(\@list); + keys %list; } =item invoicing_list_addpost @@ -1825,6 +2389,42 @@ sub open_cust_bill { grep { $_->owed > 0 } $self->cust_bill; } +=item cust_credit + +Returns all the credits (see L<FS::cust_credit>) for this customer. + +=cut + +sub cust_credit { + my $self = shift; + sort { $a->_date <=> $b->_date } + qsearch( 'cust_credit', { 'custnum' => $self->custnum } ) +} + +=item cust_pay + +Returns all the payments (see L<FS::cust_pay>) for this customer. + +=cut + +sub cust_pay { + my $self = shift; + sort { $a->_date <=> $b->_date } + qsearch( 'cust_pay', { 'custnum' => $self->custnum } ) +} + +=item cust_refund + +Returns all the refunds (see L<FS::cust_refund>) for this customer. + +=cut + +sub cust_refund { + my $self = shift; + sort { $a->_date <=> $b->_date } + qsearch( 'cust_refund', { 'custnum' => $self->custnum } ) +} + =back =head1 SUBROUTINES @@ -1964,6 +2564,201 @@ sub append_fuzzyfiles { 1; } +=item batch_import + +=cut + +sub batch_import { + my $param = shift; + #warn join('-',keys %$param); + my $fh = $param->{filehandle}; + my $agentnum = $param->{agentnum}; + my $refnum = $param->{refnum}; + my $pkgpart = $param->{pkgpart}; + my @fields = @{$param->{fields}}; + + eval "use Date::Parse;"; + die $@ if $@; + eval "use Text::CSV_XS;"; + die $@ if $@; + + my $csv = new Text::CSV_XS; + #warn $csv; + #warn $fh; + + my $imported = 0; + #my $columns; + + 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; + + #while ( $columns = $csv->getline($fh) ) { + my $line; + while ( defined($line=<$fh>) ) { + + $csv->parse($line) or do { + $dbh->rollback if $oldAutoCommit; + return "can't parse: ". $csv->error_input(); + }; + + my @columns = $csv->fields(); + #warn join('-',@columns); + + my %cust_main = ( + agentnum => $agentnum, + refnum => $refnum, + country => 'US', #default + payby => 'BILL', #default + paydate => '12/2037', #default + ); + my $billtime = time; + my %cust_pkg = ( pkgpart => $pkgpart ); + foreach my $field ( @fields ) { + if ( $field =~ /^cust_pkg\.(setup|bill|susp|expire|cancel)$/ ) { + #$cust_pkg{$1} = str2time( shift @$columns ); + if ( $1 eq 'setup' ) { + $billtime = str2time(shift @columns); + } else { + $cust_pkg{$1} = str2time( shift @columns ); + } + } else { + #$cust_main{$field} = shift @$columns; + $cust_main{$field} = shift @columns; + } + } + + my $cust_pkg = new FS::cust_pkg ( \%cust_pkg ) if $pkgpart; + my $cust_main = new FS::cust_main ( \%cust_main ); + use Tie::RefHash; + tie my %hash, 'Tie::RefHash'; #this part is important + $hash{$cust_pkg} = [] if $pkgpart; + my $error = $cust_main->insert( \%hash ); + + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "can't insert customer for $line: $error"; + } + + #false laziness w/bill.cgi + $error = $cust_main->bill( 'time' => $billtime ); + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "can't bill customer for $line: $error"; + } + + $cust_main->apply_payments; + $cust_main->apply_credits; + + $error = $cust_main->collect(); + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "can't collect customer for $line: $error"; + } + + $imported++; + } + + $dbh->commit or die $dbh->errstr if $oldAutoCommit; + + return "Empty file!" unless $imported; + + ''; #no error + +} + +=item batch_charge + +=cut + +sub batch_charge { + my $param = shift; + #warn join('-',keys %$param); + my $fh = $param->{filehandle}; + my @fields = @{$param->{fields}}; + + eval "use Date::Parse;"; + die $@ if $@; + eval "use Text::CSV_XS;"; + die $@ if $@; + + my $csv = new Text::CSV_XS; + #warn $csv; + #warn $fh; + + my $imported = 0; + #my $columns; + + 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; + + #while ( $columns = $csv->getline($fh) ) { + my $line; + while ( defined($line=<$fh>) ) { + + $csv->parse($line) or do { + $dbh->rollback if $oldAutoCommit; + return "can't parse: ". $csv->error_input(); + }; + + my @columns = $csv->fields(); + #warn join('-',@columns); + + my %row = (); + foreach my $field ( @fields ) { + $row{$field} = shift @columns; + } + + my $cust_main = qsearchs('cust_main', { 'custnum' => $row{'custnum'} } ); + unless ( $cust_main ) { + $dbh->rollback if $oldAutoCommit; + return "unknown custnum $row{'custnum'}"; + } + + if ( $row{'amount'} > 0 ) { + my $error = $cust_main->charge($row{'amount'}, $row{'pkg'}); + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + $imported++; + } elsif ( $row{'amount'} < 0 ) { + my $error = $cust_main->credit( sprintf( "%.2f", 0-$row{'amount'} ), + $row{'pkg'} ); + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + $imported++; + } else { + #hmm? + } + + } + + $dbh->commit or die $dbh->errstr if $oldAutoCommit; + + return "Empty file!" unless $imported; + + ''; #no error + +} + =back =head1 BUGS @@ -1991,4 +2786,3 @@ L<FS::cust_main_invoice>, L<FS::UID>, schema.html from the base documentation. 1; - diff --git a/FS/FS/cust_main_county.pm b/FS/FS/cust_main_county.pm index e41564d21..76c982ae8 100644 --- a/FS/FS/cust_main_county.pm +++ b/FS/FS/cust_main_county.pm @@ -61,6 +61,12 @@ currently supported: =item exempt_amount +=item taxname - if defined, printed on invoices instead of "Tax" + +=item setuptax - if 'Y', this tax does not apply to setup fees + +=item recurtax - if 'Y', this tax does not apply to recurring fees + =back =head1 METHODS @@ -110,8 +116,39 @@ sub check { || $self->ut_float('tax') || $self->ut_textn('taxclass') # ... || $self->ut_money('exempt_amount') - ; + || $self->ut_textn('taxname') + || $self->ut_enum('setuptax', [ '', 'Y' ] ) + || $self->ut_enum('recurtax', [ '', 'Y' ] ) + || $self->SUPER::check + ; + +} + +sub taxname { + my $self = shift; + if ( $self->dbdef_table->column('taxname') ) { + return $self->setfield('taxname', $_[0]) if @_; + return $self->getfield('taxname'); + } + return ''; +} + +sub setuptax { + my $self = shift; + if ( $self->dbdef_table->column('setuptax') ) { + return $self->setfield('setuptax', $_[0]) if @_; + return $self->getfield('setuptax'); + } + return ''; +} +sub recurtax { + my $self = shift; + if ( $self->dbdef_table->column('recurtax') ) { + return $self->setfield('recurtax', $_[0]) if @_; + return $self->getfield('recurtax'); + } + return ''; } =back diff --git a/FS/FS/cust_main_invoice.pm b/FS/FS/cust_main_invoice.pm index a5533a088..add0ccab1 100644 --- a/FS/FS/cust_main_invoice.pm +++ b/FS/FS/cust_main_invoice.pm @@ -107,7 +107,7 @@ sub check { return "Unknown customer" unless qsearchs('cust_main',{ 'custnum' => $self->custnum }); - ''; #noerror + $self->SUPER::check; } =item checkdest @@ -134,13 +134,6 @@ sub checkdest { unless qsearchs( 'svc_acct', { 'svcnum' => $self->dest } ); } elsif ( $self->dest =~ /^([\w\.\-\&\+]+)\@(([\w\.\-]+\.)+\w+)$/ ) { my($user, $domain) = ($1, $2); -# if ( $domain eq $mydomain ) { -# my $svc_acct = qsearchs( 'svc_acct', { 'username' => $user } ); -# return "Unknown local account: $user\@$domain (specified literally)" -# unless $svc_acct; -# $svc_acct->svcnum =~ /^(\d+)$/ or die "Non-numeric svcnum?!"; -# $self->dest($1); -# } $self->dest("$1\@$2"); } else { return gettext("illegal_email_invoice_address"); @@ -170,7 +163,7 @@ sub address { =head1 VERSION -$Id: cust_main_invoice.pm,v 1.12 2002-04-12 13:22:02 ivan Exp $ +$Id: cust_main_invoice.pm,v 1.14 2003-08-05 00:20:42 khoff Exp $ =head1 BUGS diff --git a/FS/FS/cust_pay.pm b/FS/FS/cust_pay.pm index 98eba704b..e1943ae2d 100644 --- a/FS/FS/cust_pay.pm +++ b/FS/FS/cust_pay.pm @@ -1,13 +1,12 @@ package FS::cust_pay; use strict; -use vars qw( @ISA $conf $unsuspendauto $smtpmachine $invoice_from ); +use vars qw( @ISA $conf $unsuspendauto ); use Date::Format; -use Mail::Header; -use Mail::Internet 1.44; use Business::CreditCard; use FS::UID qw( dbh ); use FS::Record qw( dbh qsearch qsearchs dbh ); +use FS::Misc qw(send_email); use FS::cust_bill; use FS::cust_bill_pay; use FS::cust_main; @@ -15,14 +14,10 @@ use FS::cust_main; @ISA = qw( FS::Record ); #ask FS::UID to run this stuff for us later -$FS::UID::callback{'FS::cust_pay'} = sub { - +FS::UID->install_callback( sub { $conf = new FS::Conf; $unsuspendauto = $conf->exists('unsuspendauto'); - $smtpmachine = $conf->config('smtpmachine'); - $invoice_from = $conf->config('invoice_from'); - -}; +} ); =head1 NAME @@ -60,7 +55,8 @@ currently supported: =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion functions. -=item payby - `CARD' (credit cards), `BILL' (billing), or `COMP' (free) +=item payby - `CARD' (credit cards), `CHEK' (electronic check/ACH), +`LECB' (phone bill billing), `BILL' (billing), or `COMP' (free) =item payinfo - card number, check #, or comp issuer (4-8 lowercase alphanumerics; think username), respectively @@ -264,19 +260,12 @@ sub delete { if ( $conf->config('deletepayments') ne '' ) { my $cust_main = qsearchs('cust_main',{ 'custnum' => $self->custnum }); - #false laziness w/FS::cust_bill::send & fs_signup_server - $ENV{MAILADDRESS} = $invoice_from; #??? well as good as any - my $header = new Mail::Header ( [ - "From: $invoice_from", - "To: ". $conf->config('deletepayments'), - "Sender: $invoice_from", - "Reply-To: $invoice_from", - "Date: ". time2str("%a, %d %b %Y %X %z", time), - "Subject: FREESIDE NOTIFICATION: Payment deleted", - ] ); - my $message = new Mail::Internet ( - 'Header' => $header, - 'Body' => [ + + my $error = send_email( + 'from' => $conf->config('invoice_from'), #??? well as good as any + 'to' => $conf->config('deletepayments'), + 'subject' => 'FREESIDE NOTIFICATION: Payment deleted', + 'body' => [ "This is an automatic message from your Freeside installation\n", "informing you that the following payment has been deleted:\n", "\n", @@ -290,16 +279,12 @@ sub delete { 'paybatch: '. $self->paybatch. "\n", ], ); - $!=0; - $message->smtpsend( Host => $smtpmachine ) - or $message->smtpsend( Host => $smtpmachine, Debug => 1 ) - or do { - $dbh->rollback if $oldAutoCommit; - return "(customer # ". $self->custnum. - ") can't send payment deletion email to ". - $conf->config('deletepayments'). - " via server $smtpmachine with SMTP: $!"; - }; + + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "can't send payment deletion notification: $error"; + } + } $dbh->commit or die $dbh->errstr if $oldAutoCommit; @@ -346,7 +331,7 @@ sub check { $self->_date(time) unless $self->_date; - $self->payby =~ /^(CARD|BILL|COMP)$/ or return "Illegal payby"; + $self->payby =~ /^(CARD|CHEK|LECB|BILL|COMP)$/ or return "Illegal payby"; $self->payby($1); #false laziness with cust_refund::check @@ -369,8 +354,7 @@ sub check { return $error if $error; } - ''; #no error - + $self->SUPER::check; } =item cust_bill_pay @@ -401,11 +385,23 @@ sub unapplied { sprintf("%.2f", $amount ); } +=item cust_main + +Returns the parent customer object (see L<FS::cust_main>). + +=cut + +sub cust_main { + my $self = shift; + qsearchs( 'cust_main', { 'custnum' => $self->custnum } ); +} + + =back =head1 VERSION -$Id: cust_pay.pm,v 1.21 2002-06-04 14:35:52 ivan Exp $ +$Id: cust_pay.pm,v 1.26 2003-09-10 10:54:46 ivan Exp $ =head1 BUGS diff --git a/FS/FS/cust_pay_batch.pm b/FS/FS/cust_pay_batch.pm index c4427c387..8059f1ca2 100644 --- a/FS/FS/cust_pay_batch.pm +++ b/FS/FS/cust_pay_batch.pm @@ -2,7 +2,7 @@ package FS::cust_pay_batch; use strict; use vars qw( @ISA ); -use FS::Record; +use FS::Record qw(dbh qsearchs); use Business::CreditCard; @ISA = qw( FS::Record ); @@ -185,14 +185,202 @@ sub check { #check invnum, custnum, ? - ''; #no error + $self->SUPER::check; +} + +=item cust_main + +Returns the customer (see L<FS::cust_main>) for this batched credit card +payment. + +=cut + +sub cust_main { + my $self = shift; + qsearchs( 'cust_main', { 'custnum' => $self->custnum } ); } =back -=head1 VERSION +=head1 SUBROUTINES + +=over 4 + +=item import_results + +=cut + +sub import_results { + use Time::Local; + use FS::cust_pay; + eval "use Text::CSV_XS;"; + die $@ if $@; +# + my $param = shift; + my $fh = $param->{'filehandle'}; + my $format = $param->{'format'}; + my $paybatch = $param->{'paybatch'}; + + my @fields; + my $end_condition; + my $end_hook; + my $hook; + my $approved_condition; + my $declined_condition; + + if ( $format eq 'csv-td_canada_trust-merchant_pc_batch' ) { + + @fields = ( + 'paybatchnum', # Reference#: Invoice number of the transaction + 'paid', # Amount: Amount of the transaction. Dollars and cents + # with no decimal entered. + '', # Card Type: 0 - MCrd, 1 - Visa, 2 - AMEX, 3 - Discover, + # 4 - Insignia, 5 - Diners/EnRoute, 6 - JCB + '_date', # Transaction Date: Date the Transaction was processed + 'time', # Transaction Time: Time the transaction was processed + 'payinfo', # Card Number: Card number for the transaction + '', # Expiry Date: Expiry date of the card + '', # Auth#: Authorization number entered for force post + # transaction + 'type', # Transaction Type: 0 - purchase, 40 - refund, + # 20 - force post + 'result', # Processing Result: 3 - Approval, + # 4 - Declined/Amount over limit, + # 5 - Invalid/Expired/stolen card, + # 6 - Comm Error + '', # Terminal ID: Terminal ID used to process the transaction + ); + + $end_condition = sub { + my $hash = shift; + $hash->{'type'} eq '0BC'; + }; + + $end_hook = sub { + my( $hash, $total) = @_; + $total = sprintf("%.2f", $total); + my $batch_total = sprintf("%.2f", $hash->{'paybatchnum'} / 100 ); + return "Our total $total does not match bank total $batch_total!" + if $total != $batch_total; + ''; + }; + + $hook = sub { + my $hash = shift; + $hash->{'paid'} = sprintf("%.2f", $hash->{'paid'} / 100 ); + $hash->{'_date'} = timelocal( substr($hash->{'time'}, 4, 2), + substr($hash->{'time'}, 2, 2), + substr($hash->{'time'}, 0, 2), + substr($hash->{'_date'}, 6, 2), + substr($hash->{'_date'}, 4, 2)-1, + substr($hash->{'_date'}, 0, 4)-1900, ); + }; + + $approved_condition = sub { + my $hash = shift; + $hash->{'type'} eq '0' && $hash->{'result'} == 3; + }; + + $declined_condition = sub { + my $hash = shift; + $hash->{'type'} eq '0' && ( $hash->{'result'} == 4 + || $hash->{'result'} == 5 ); + }; + + + } else { + return "Unknown format $format"; + } + + my $csv = new Text::CSV_XS; + + 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 $total = 0; + my $line; + while ( defined($line=<$fh>) ) { + + next if $line =~ /^\s*$/; #skip blank lines -$Id: cust_pay_batch.pm,v 1.6 2002-02-22 23:08:11 ivan Exp $ + $csv->parse($line) or do { + $dbh->rollback if $oldAutoCommit; + return "can't parse: ". $csv->error_input(); + }; + + my @values = $csv->fields(); + my %hash; + foreach my $field ( @fields ) { + my $value = shift @values; + next unless $field; + $hash{$field} = $value; + } + + if ( &{$end_condition}(\%hash) ) { + my $error = &{$end_hook}(\%hash, $total); + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + last; + } + + my $cust_pay_batch = + qsearchs('cust_pay_batch', { 'paybatchnum' => $hash{'paybatchnum'} } ); + unless ( $cust_pay_batch ) { + $dbh->rollback if $oldAutoCommit; + return "unknown paybatchnum $hash{'paybatchnum'}\n"; + } + my $custnum = $cust_pay_batch->custnum, + + my $error = $cust_pay_batch->delete; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "error removing paybatchnum $hash{'paybatchnum'}: $error\n"; + } + + &{$hook}(\%hash); + + if ( &{$approved_condition}(\%hash) ) { + + my $cust_pay = new FS::cust_pay ( { + 'custnum' => $custnum, + 'payby' => 'CARD', + 'paybatch' => $paybatch, + map { $_ => $hash{$_} } (qw( paid _date payinfo )), + } ); + $error = $cust_pay->insert; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "error adding payment paybatchnum $hash{'paybatchnum'}: $error\n"; + } + $total += $hash{'paid'}; + + $cust_pay->cust_main->apply_payments; + + } elsif ( &{$declined_condition}(\%hash) ) { + + #this should be configurable... if anybody else ever uses batches + $cust_pay_batch->cust_main->suspend; + + } + + } + + $dbh->commit or die $dbh->errstr if $oldAutoCommit; + ''; + +} + +=back =head1 BUGS diff --git a/FS/FS/cust_pkg.pm b/FS/FS/cust_pkg.pm index 8b65ac4bd..c2182118f 100644 --- a/FS/FS/cust_pkg.pm +++ b/FS/FS/cust_pkg.pm @@ -1,26 +1,34 @@ package FS::cust_pkg; use strict; -use vars qw(@ISA); +use vars qw(@ISA $disable_agentcheck $DEBUG); use FS::UID qw( getotaker dbh ); use FS::Record qw( qsearch qsearchs ); +use FS::Misc qw( send_email ); use FS::cust_svc; use FS::part_pkg; use FS::cust_main; use FS::type_pkgs; use FS::pkg_svc; +use FS::cust_bill_pkg; # need to 'use' these instead of 'require' in sub { cancel, suspend, unsuspend, # setup } # because they load configuraion by setting FS::UID::callback (see TODO) use FS::svc_acct; -use FS::svc_acct_sm; use FS::svc_domain; use FS::svc_www; use FS::svc_forward; +# for sending cancel emails in sub cancel +use FS::Conf; + @ISA = qw( FS::Record ); +$DEBUG = 0; + +$disable_agentcheck = 0; + sub _cache { my $self = shift; my ( $hashref, $cache ) = @_; @@ -91,7 +99,9 @@ inherits from FS::Record. The following fields are currently supported: =item setup - date -=item bill - date +=item bill - date (next bill date) + +=item last_bill - last bill date =item susp - date @@ -140,12 +150,15 @@ sub insert { return $error if $error; my $cust_main = $self->cust_main; - return "Unknown customer ". $self->custnum unless $cust_main; - - my $agent = qsearchs( 'agent', { 'agentnum' => $cust_main->agentnum } ); - my $pkgpart_href = $agent->pkgpart_hashref; - return "agent ". $agent->agentnum. " can't purchase pkgpart ". $self->pkgpart - unless $pkgpart_href->{ $self->pkgpart }; + return "Unknown custnum: ". $self->custnum unless $cust_main; + + unless ( $disable_agentcheck ) { + my $agent = qsearchs( 'agent', { 'agentnum' => $cust_main->agentnum } ); + my $pkgpart_href = $agent->pkgpart_hashref; + return "agent ". $agent->agentnum. + " can't purchase pkgpart ". $self->pkgpart + unless $pkgpart_href->{ $self->pkgpart }; + } $self->SUPER::insert; @@ -229,29 +242,35 @@ sub check { unless qsearchs( 'part_pkg', { 'pkgpart' => $self->pkgpart } ); $self->otaker(getotaker) unless $self->otaker; - $self->otaker =~ /^(\w{0,16})$/ or return "Illegal otaker"; + $self->otaker =~ /^([\w\.\-]{0,16})$/ or return "Illegal otaker"; $self->otaker($1); if ( $self->dbdef_table->column('manual_flag') ) { - $self->manual_flag =~ /^([01]?)$/ or return "Illegal manual_flag"; + $self->manual_flag('') if $self->manual_flag eq ' '; + $self->manual_flag =~ /^([01]?)$/ + or return "Illegal manual_flag ". $self->manual_flag; $self->manual_flag($1); } - ''; #no error + $self->SUPER::check; } -=item cancel +=item cancel [ OPTION => VALUE ... ] Cancels and removes all services (see L<FS::cust_svc> and L<FS::part_svc>) in this package, then cancels the package itself (sets the cancel field to now). +Available options are: I<quiet> + +I<quiet> can be set true to supress email cancellation notices. + If there is an error, returns the error, otherwise returns false. =cut sub cancel { - my $self = shift; + my( $self, %options ) = @_; my $error; local $SIG{HUP} = 'IGNORE'; @@ -290,7 +309,21 @@ sub cancel { $dbh->commit or die $dbh->errstr if $oldAutoCommit; + my $conf = new FS::Conf; + my @invoicing_list = grep { $_ ne 'POST' } $self->cust_main->invoicing_list; + if ( !$options{'quiet'} && $conf->exists('emailcancel') && @invoicing_list ) { + my $conf = new FS::Conf; + my $error = send_email( + 'from' => $conf->config('invoice_from'), + 'to' => \@invoicing_list, + 'subject' => $conf->config('cancelsubject'), + 'body' => [ map "$_\n", $conf->config('cancelmessage') ], + ); + #should this do something on errors? + } + ''; #no errors + } =item suspend @@ -419,6 +452,24 @@ sub unsuspend { ''; #no errors } +=item last_bill + +Returns the last bill date, or if there is no last bill date, the setup date. +Useful for billing metered services. + +=cut + +sub last_bill { + my $self = shift; + if ( $self->dbdef_table->column('last_bill') ) { + return $self->setfield('last_bill', $_[0]) if @_; + return $self->getfield('last_bill') if $self->getfield('last_bill'); + } + my $cust_bill_pkg = qsearchs('cust_bill_pkg', { 'pkgnum' => $self->pkgnum, + 'edate' => $self->bill, } ); + $cust_bill_pkg ? $cust_bill_pkg->sdate : $self->setup || 0; +} + =item part_pkg Returns the definition for this billing item, as an FS::part_pkg object (see @@ -476,7 +527,7 @@ sub cust_main { =item seconds_since TIMESTAMP Returns the number of seconds all accounts (see L<FS::svc_acct>) in this -package have been online since TIMESTAMP. +package have been online since TIMESTAMP, according to the session monitor. TIMESTAMP is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion functions. @@ -497,6 +548,160 @@ sub seconds_since { } +=item seconds_since_sqlradacct TIMESTAMP_START TIMESTAMP_END + +Returns the numbers of seconds all accounts (see L<FS::svc_acct>) in this +package have been online between TIMESTAMP_START (inclusive) and TIMESTAMP_END +(exclusive). + +TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see +L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion +functions. + + +=cut + +sub seconds_since_sqlradacct { + my($self, $start, $end) = @_; + + my $seconds = 0; + + foreach my $cust_svc ( + grep { + my $part_svc = $_->part_svc; + $part_svc->svcdb eq 'svc_acct' + && scalar($part_svc->part_export('sqlradius')); + } $self->cust_svc + ) { + $seconds += $cust_svc->seconds_since_sqlradacct($start, $end); + } + + $seconds; + +} + +=item attribute_since_sqlradacct TIMESTAMP_START TIMESTAMP_END ATTRIBUTE + +Returns the sum of the given attribute for all accounts (see L<FS::svc_acct>) +in this package for sessions ending between TIMESTAMP_START (inclusive) and +TIMESTAMP_END +(exclusive). + +TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see +L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion +functions. + +=cut + +sub attribute_since_sqlradacct { + my($self, $start, $end, $attrib) = @_; + + my $sum = 0; + + foreach my $cust_svc ( + grep { + my $part_svc = $_->part_svc; + $part_svc->svcdb eq 'svc_acct' + && scalar($part_svc->part_export('sqlradius')); + } $self->cust_svc + ) { + $sum += $cust_svc->attribute_since_sqlradacct($start, $end, $attrib); + } + + $sum; + +} + +=item transfer DEST_PKGNUM + +Transfers as many services as possible from this package to another package. +The destination package must already exist. Services are moved only if +the destination allows services with the correct I<svcpart> (not svcdb). +Any services that can't be moved remain in the original package. + +Returns an error, if there is one; otherwise, returns the number of services +that couldn't be moved. + +=cut + +sub transfer { + my ($self, $dest_pkgnum) = @_; + + my $remaining = 0; + my $dest; + my %target; + my $pkg_svc; + + if (ref ($dest_pkgnum) eq 'FS::cust_pkg') { + $dest = $dest_pkgnum; + $dest_pkgnum = $dest->pkgnum; + } else { + $dest = qsearchs('cust_pkg', { pkgnum => $dest_pkgnum }); + } + + return ('Package does not exist: '.$dest_pkgnum) unless $dest; + + foreach $pkg_svc (qsearch('pkg_svc', { pkgpart => $dest->pkgpart })) { + $target{$pkg_svc->svcpart} = $pkg_svc->quantity; + } + + my $cust_svc; + + foreach $cust_svc ($dest->cust_svc) { + $target{$cust_svc->svcpart}--; + } + + foreach $cust_svc ($self->cust_svc) { + if($target{$cust_svc->svcpart} > 0) { + $target{$cust_svc->svcpart}--; + my $new = new FS::cust_svc { + svcnum => $cust_svc->svcnum, + svcpart => $cust_svc->svcpart, + pkgnum => $dest_pkgnum }; + my $error = $new->replace($cust_svc); + return $error if $error; + } else { + $remaining++ + } + } + return $remaining; +} + +=item reexport + +=cut + +sub reexport { + 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; + + foreach my $cust_svc ( $self->cust_svc ) { + #false laziness w/svc_Common::insert + my $svc_x = $cust_svc->svc_x; + foreach my $part_export ( $cust_svc->part_svc->part_export ) { + my $error = $part_export->export_insert($svc_x); + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + } + } + + $dbh->commit or die $dbh->errstr if $oldAutoCommit; + ''; + +} + =back =head1 SUBROUTINES @@ -523,97 +728,9 @@ newly-created cust_pkg objects. =cut sub order { - my($custnum, $pkgparts, $remove_pkgnums, $return_cust_pkg) = @_; - $remove_pkgnums = [] unless defined($remove_pkgnums); - - my $oldAutoCommit = $FS::UID::AutoCommit; - local $FS::UID::AutoCommit = 0; - my $dbh = dbh; - - # generate %part_pkg - # $part_pkg{$pkgpart} is true iff $custnum may purchase $pkgpart - # - my($cust_main)=qsearchs('cust_main',{'custnum'=>$custnum}); - my($agent)=qsearchs('agent',{'agentnum'=> $cust_main->agentnum }); - my %part_pkg = %{ $agent->pkgpart_hashref }; - - my(%svcnum); - # generate %svcnum - # for those packages being removed: - #@{ $svcnum{$svcpart} } goes from a svcpart to a list of FS::cust_svc objects - my($pkgnum); - foreach $pkgnum ( @{$remove_pkgnums} ) { - foreach my $cust_svc (qsearch('cust_svc',{'pkgnum'=>$pkgnum})) { - push @{ $svcnum{$cust_svc->getfield('svcpart')} }, $cust_svc; - } - } - - my @cust_svc; - #generate @cust_svc - # for those packages the customer is purchasing: - # @{$pkgparts} is a list of said packages, by pkgpart - # @cust_svc is a corresponding list of lists of FS::Record objects - foreach my $pkgpart ( @{$pkgparts} ) { - unless ( $part_pkg{$pkgpart} ) { - $dbh->rollback if $oldAutoCommit; - return "Customer not permitted to purchase pkgpart $pkgpart!"; - } - push @cust_svc, [ - map { - ( $svcnum{$_} && @{ $svcnum{$_} } ) ? shift @{ $svcnum{$_} } : (); - } map { $_->svcpart } - qsearch('pkg_svc', { pkgpart => $pkgpart, - quantity => { op=>'>', value=>'0', } } ) - ]; - } + my ($custnum, $pkgparts, $remove_pkgnum, $return_cust_pkg) = @_; - #special-case until this can be handled better - # move services to new svcparts - even if the svcparts don't match (svcdb - # needs to...) - # looks like they're moved in no particular order, ewwwwwwww - # and looks like just one of each svcpart can be moved... o well - - #start with still-leftover services - #foreach my $svcpart ( grep { scalar(@{ $svcnum{$_} }) } keys %svcnum ) { - foreach my $svcpart ( keys %svcnum ) { - next unless @{ $svcnum{$svcpart} }; - - my $svcdb = $svcnum{$svcpart}->[0]->part_svc->svcdb; - - #find an empty place to put one - my $i = 0; - foreach my $pkgpart ( @{$pkgparts} ) { - my @pkg_svc = - qsearch('pkg_svc', { pkgpart => $pkgpart, - quantity => { op=>'>', value=>'0', } } ); - #my @pkg_svc = - # grep { $_->quantity > 0 } qsearch('pkg_svc', { pkgpart=>$pkgpart } ); - if ( ! @{$cust_svc[$i]} #find an empty place to put them with - && grep { $svcdb eq $_->part_svc->svcdb } #with appropriate svcdb - @pkg_svc - ) { - my $new_svcpart = - ( grep { $svcdb eq $_->part_svc->svcdb } @pkg_svc )[0]->svcpart; - my $cust_svc = shift @{$svcnum{$svcpart}}; - $cust_svc->svcpart($new_svcpart); - #warn "changing from $svcpart to $new_svcpart!!!\n"; - $cust_svc[$i] = [ $cust_svc ]; - } - $i++; - } - - } - - #check for leftover services - foreach (keys %svcnum) { - next unless @{ $svcnum{$_} }; - $dbh->rollback if $oldAutoCommit; - return "Leftover services, svcpart $_: svcnum ". - join(', ', map { $_->svcnum } @{ $svcnum{$_} } ); - } - - #no leftover services, let's make changes. - + # Transactionize this whole mess local $SIG{HUP} = 'IGNORE'; local $SIG{INT} = 'IGNORE'; local $SIG{QUIT} = 'IGNORE'; @@ -621,66 +738,59 @@ sub order { local $SIG{TSTP} = 'IGNORE'; local $SIG{PIPE} = 'IGNORE'; - #first cancel old packages - foreach my $pkgnum ( @{$remove_pkgnums} ) { - my($old) = qsearchs('cust_pkg',{'pkgnum'=>$pkgnum}); - unless ( $old ) { - $dbh->rollback if $oldAutoCommit; - return "Package $pkgnum not found to remove!"; - } - my(%hash) = $old->hash; - $hash{'cancel'}=time; - my($new) = new FS::cust_pkg ( \%hash ); - my($error)=$new->replace($old); - if ( $error ) { + my $oldAutoCommit = $FS::UID::AutoCommit; + local $FS::UID::AutoCommit = 0; + my $dbh = dbh; + + my $error; + my $cust_main = qsearchs('cust_main', { custnum => $custnum }); + return "Customer not found: $custnum" unless $cust_main; + + # Create the new packages. + my $cust_pkg; + foreach (@$pkgparts) { + $cust_pkg = new FS::cust_pkg { custnum => $custnum, + pkgpart => $_ }; + $error = $cust_pkg->insert; + if ($error) { $dbh->rollback if $oldAutoCommit; - return "Couldn't update package $pkgnum: $error"; + return $error; } + push @$return_cust_pkg, $cust_pkg; } - - #now add new packages, changing cust_svc records if necessary - my $pkgpart; - while ($pkgpart=shift @{$pkgparts} ) { - - my $new = new FS::cust_pkg { - 'custnum' => $custnum, - 'pkgpart' => $pkgpart, - }; - my $error = $new->insert; - if ( $error ) { + # $return_cust_pkg now contains refs to all of the newly + # created packages. + + # Transfer services and cancel old packages. + foreach my $old_pkgnum (@$remove_pkgnum) { + my $old_pkg = qsearchs ('cust_pkg', { pkgnum => $old_pkgnum }); + foreach my $new_pkg (@$return_cust_pkg) { + $error = $old_pkg->transfer($new_pkg); + if ($error and $error == 0) { + # $old_pkg->transfer failed. + $dbh->rollback if $oldAutoCommit; + return $error; + } + } + if ($error > 0) { + # Transfers were successful, but we went through all of the + # new packages and still had services left on the old package. + # We can't cancel the package under the circumstances, so abort. $dbh->rollback if $oldAutoCommit; - return "Couldn't insert new cust_pkg record: $error"; + return "Unable to transfer all services from package ".$old_pkg->pkgnum; } - push @{$return_cust_pkg}, $new if $return_cust_pkg; - my $pkgnum = $new->pkgnum; - - foreach my $cust_svc ( @{ shift @cust_svc } ) { - my(%hash) = $cust_svc->hash; - $hash{'pkgnum'}=$pkgnum; - my $new = new FS::cust_svc ( \%hash ); - - #avoid Record diffing missing changed svcpart field from above. - my $old = qsearchs('cust_svc', { 'svcnum' => $cust_svc->svcnum } ); - - my $error = $new->replace($old); - if ( $error ) { - $dbh->rollback if $oldAutoCommit; - return "Couldn't link old service to new package: $error"; - } + $error = $old_pkg->cancel; + if ($error) { + $dbh->rollback; + return $error; } - } - + } $dbh->commit or die $dbh->errstr if $oldAutoCommit; - - ''; #no errors + ''; } =back -=head1 VERSION - -$Id: cust_pkg.pm,v 1.22 2002-05-22 12:17:06 ivan Exp $ - =head1 BUGS sub order is not OO. Perhaps it should be moved to FS::cust_main and made so? @@ -690,11 +800,12 @@ In sub order, the @pkgparts array (passed by reference) is clobbered. Also in sub order, no money is adjusted. Once FS::part_pkg defines a standard method to pass dates to the recur_prog expression, it should do so. -FS::svc_acct, FS::svc_acct_sm, and FS::svc_domain are loaded via 'use' at -compile time, rather than via 'require' in sub { setup, suspend, unsuspend, -cancel } because they use %FS::UID::callback to load configuration values. -Probably need a subroutine which decides what to do based on whether or not -we've fetched the user yet, rather than a hash. See FS::UID and the TODO. +FS::svc_acct, FS::svc_domain, FS::svc_www, FS::svc_ip and FS::svc_forward are +loaded via 'use' at compile time, rather than via 'require' in sub { setup, +suspend, unsuspend, cancel } because they use %FS::UID::callback to load +configuration values. Probably need a subroutine which decides what to do +based on whether or not we've fetched the user yet, rather than a hash. See +FS::UID and the TODO. Now that things are transactional should the check in the insert method be moved to check ? diff --git a/FS/FS/cust_refund.pm b/FS/FS/cust_refund.pm index 8fe6876d3..250bd20e0 100644 --- a/FS/FS/cust_refund.pm +++ b/FS/FS/cust_refund.pm @@ -47,7 +47,8 @@ inherits from FS::Record. The following fields are currently supported: =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion functions. -=item payby - `CARD' (credit cards), `BILL' (billing), or `COMP' (free) +=item payby - `CARD' (credit cards), `CHEK' (electronic check/ACH), +`LECB' (Phone bill billing), `BILL' (billing), or `COMP' (free) =item payinfo - card number, P.O.#, or comp issuer (4-8 lowercase alphanumerics; think username) @@ -234,7 +235,7 @@ sub check { unless $self->crednum || qsearchs( 'cust_main', { 'custnum' => $self->custnum } ); - $self->payby =~ /^(CARD|BILL|COMP)$/ or return "Illegal payby"; + $self->payby =~ /^(CARD|CHEK|LECB|BILL|COMP)$/ or return "Illegal payby"; $self->payby($1); #false laziness with cust_pay::check @@ -259,14 +260,14 @@ sub check { $self->otaker(getotaker); - ''; #no error + $self->SUPER::check; } =back =head1 VERSION -$Id: cust_refund.pm,v 1.18 2002-02-19 03:22:39 jeff Exp $ +$Id: cust_refund.pm,v 1.21 2003-08-05 00:20:42 khoff Exp $ =head1 BUGS diff --git a/FS/FS/cust_svc.pm b/FS/FS/cust_svc.pm index c7cc4b322..0d8a12114 100644 --- a/FS/FS/cust_svc.pm +++ b/FS/FS/cust_svc.pm @@ -1,7 +1,7 @@ package FS::cust_svc; use strict; -use vars qw( @ISA ); +use vars qw( @ISA $ignore_quantity ); use Carp qw( cluck ); use FS::Record qw( qsearch qsearchs dbh ); use FS::cust_pkg; @@ -9,13 +9,16 @@ use FS::part_pkg; use FS::part_svc; use FS::pkg_svc; use FS::svc_acct; -use FS::svc_acct_sm; use FS::svc_domain; use FS::svc_forward; +use FS::svc_broadband; use FS::domain_record; +use FS::part_export; @ISA = qw( FS::Record ); +$ignore_quantity = 0; + sub _cache { my $self = shift; my ( $hashref, $cache ) = @_; @@ -220,6 +223,7 @@ sub check { # 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, @@ -227,10 +231,10 @@ sub check { }); return "Already ". scalar(@cust_svc). " ". $part_svc->svc. " services for pkgnum ". $self->pkgnum - if scalar(@cust_svc) >= $pkg_svc->quantity; + if scalar(@cust_svc) >= $quantity && !$ignore_quantity; } - ''; #no error + $self->SUPER::check; } =item part_svc @@ -276,16 +280,16 @@ sub label { my $tag; if ( $svcdb eq 'svc_acct' ) { $tag = $svc_x->email; - } elsif ( $svcdb eq 'svc_acct_sm' ) { - my $domuser = $svc_x->domuser eq '*' ? '(anything)' : $svc_x->domuser; - my $svc_domain = qsearchs ( 'svc_domain', { 'svcnum' => $svc_x->domsvc } ); - my $domain = $svc_domain->domain; - $tag = "$domuser\@$domain"; } elsif ( $svcdb eq 'svc_forward' ) { - my $svc_acct = qsearchs( 'svc_acct', { 'svcnum' => $svc_x->srcsvc } ); - $tag = $svc_acct->email. '->'; + if ( $svc_x->srcsvc ) { + my $svc_acct = $svc_x->srcsvc_acct; + $tag = $svc_acct->email; + } else { + $tag = $svc_x->src; + } + $tag .= '->'; if ( $svc_x->dstsvc ) { - $svc_acct = qsearchs( 'svc_acct', { 'svcnum' => $svc_x->dstsvc } ); + my $svc_acct = $svc_x->dstsvc_acct; $tag .= $svc_acct->email; } else { $tag .= $svc_x->dst; @@ -294,7 +298,11 @@ sub label { $tag = $svc_x->getfield('domain'); } elsif ( $svcdb eq 'svc_www' ) { my $domain = qsearchs( 'domain_record', { 'recnum' => $svc_x->recnum } ); - $tag = $domain->reczone; + $tag = $domain->zone; + } elsif ( $svcdb eq 'svc_broadband' ) { + $tag = $svc_x->ip_addr; + } elsif ( $svcdb eq 'svc_external' ) { + $tag = $svc_x->id. ': '. $svc_x->title; } else { cluck "warning: asked for label of unsupported svcdb; using svcnum"; $tag = $svc_x->getfield('svcnum'); @@ -340,11 +348,249 @@ sub seconds_since { $sth->fetchrow_arrayref->[0]; } -=back +=item seconds_since_sqlradacct TIMESTAMP_START TIMESTAMP_END + +See L<FS::svc_acct/seconds_since_sqlradacct>. Equivalent to +$cust_svc->svc_x->seconds_since_sqlradacct, but more efficient. Meaningless +for records where B<svcdb> is not "svc_acct". + +=cut + +#note: implementation here, POD in FS::svc_acct +sub seconds_since_sqlradacct { + my($self, $start, $end) = @_; + + my $svc_x = $self->svc_x; + + my @part_export = $self->part_svc->part_export('sqlradius'); + push @part_export, $self->part_svc->part_export('sqlradius_withdomain'); + die "no sqlradius or sqlradius_withdomain export configured for this". + "service type" + unless @part_export; + #or return undef; + + my $seconds = 0; + foreach my $part_export ( @part_export ) { + + next if $part_export->option('ignore_accounting'); + + my $dbh = DBI->connect( map { $part_export->option($_) } + qw(datasrc username password) ) + or die "can't connect to sqlradius database: ". $DBI::errstr; + + #select a unix time conversion function based on database type + my $str2time; + if ( $dbh->{Driver}->{Name} eq 'mysql' ) { + $str2time = 'UNIX_TIMESTAMP('; + } elsif ( $dbh->{Driver}->{Name} eq 'Pg' ) { + $str2time = 'EXTRACT( EPOCH FROM '; + } else { + warn "warning: unknown database type ". $dbh->{Driver}->{Name}. + "; guessing how to convert to UNIX timestamps"; + $str2time = 'extract(epoch from '; + } + + my $username; + if ( $part_export->exporttype eq 'sqlradius' ) { + $username = $svc_x->username; + } elsif ( $part_export->exporttype eq 'sqlradius_withdomain' ) { + $username = $svc_x->email; + } else { + die 'unknown exporttype '. $part_export->exporttype; + } + + my $query; + + #find closed sessions completely within the given range + my $sth = $dbh->prepare("SELECT SUM(acctsessiontime) + FROM radacct + WHERE UserName = ? + AND $str2time AcctStartTime) >= ? + AND $str2time AcctStopTime ) < ? + AND $str2time AcctStopTime ) > 0 + AND AcctStopTime IS NOT NULL" + ) or die $dbh->errstr; + $sth->execute($username, $start, $end) or die $sth->errstr; + my $regular = $sth->fetchrow_arrayref->[0]; + + #find open sessions which start in the range, count session start->range end + $query = "SELECT SUM( ? - $str2time AcctStartTime ) ) + FROM radacct + WHERE UserName = ? + AND $str2time AcctStartTime ) >= ? + AND $str2time AcctStartTime ) < ? + AND ( ? - $str2time AcctStartTime ) ) < 86400 + AND ( $str2time AcctStopTime ) = 0 + OR AcctStopTime IS NULL )"; + $sth = $dbh->prepare($query) or die $dbh->errstr; + $sth->execute($end, $username, $start, $end, $end) + or die $sth->errstr. " executing query $query"; + my $start_during = $sth->fetchrow_arrayref->[0]; + + #find closed sessions which start before the range but stop during, + #count range start->session end + $sth = $dbh->prepare("SELECT SUM( $str2time AcctStopTime ) - ? ) + FROM radacct + WHERE UserName = ? + AND $str2time AcctStartTime ) < ? + AND $str2time AcctStopTime ) >= ? + AND $str2time AcctStopTime ) < ? + AND $str2time AcctStopTime ) > 0 + AND AcctStopTime IS NOT NULL" + ) or die $dbh->errstr; + $sth->execute($start, $username, $start, $start, $end ) or die $sth->errstr; + my $end_during = $sth->fetchrow_arrayref->[0]; + + #find closed (not anymore - or open) sessions which start before the range + # but stop after, or are still open, count range start->range end + # don't count open sessions (probably missing stop record) + $sth = $dbh->prepare("SELECT COUNT(*) + FROM radacct + WHERE UserName = ? + AND $str2time AcctStartTime ) < ? + AND ( $str2time AcctStopTime ) >= ? + )" + # OR AcctStopTime = 0 + # OR AcctStopTime IS NULL )" + ) or die $dbh->errstr; + $sth->execute($username, $start, $end ) or die $sth->errstr; + my $entire_range = ($end-$start) * $sth->fetchrow_arrayref->[0]; + + $seconds += $regular + $end_during + $start_during + $entire_range; + + } + + $seconds; + +} + +=item attribute_since_sqlradacct TIMESTAMP_START TIMESTAMP_END ATTRIBUTE + +See L<FS::svc_acct/attribute_since_sqlradacct>. Equivalent to +$cust_svc->svc_x->attribute_since_sqlradacct, but more efficient. Meaningless +for records where B<svcdb> is not "svc_acct". + +=cut + +#note: implementation here, POD in FS::svc_acct +#(false laziness w/seconds_since_sqlradacct above) +sub attribute_since_sqlradacct { + my($self, $start, $end, $attrib) = @_; -=head1 VERSION + my $svc_x = $self->svc_x; -$Id: cust_svc.pm,v 1.15 2002-05-22 12:17:06 ivan Exp $ + my @part_export = $self->part_svc->part_export('sqlradius'); + push @part_export, $self->part_svc->part_export('sqlradius_withdomain'); + die "no sqlradius or sqlradius_withdomain export configured for this". + "service type" + unless @part_export; + #or return undef; + + my $sum = 0; + + foreach my $part_export ( @part_export ) { + + next if $part_export->option('ignore_accounting'); + + my $dbh = DBI->connect( map { $part_export->option($_) } + qw(datasrc username password) ) + or die "can't connect to sqlradius database: ". $DBI::errstr; + + #select a unix time conversion function based on database type + my $str2time; + if ( $dbh->{Driver}->{Name} eq 'mysql' ) { + $str2time = 'UNIX_TIMESTAMP('; + } elsif ( $dbh->{Driver}->{Name} eq 'Pg' ) { + $str2time = 'EXTRACT( EPOCH FROM '; + } else { + warn "warning: unknown database type ". $dbh->{Driver}->{Name}. + "; guessing how to convert to UNIX timestamps"; + $str2time = 'extract(epoch from '; + } + + my $username; + if ( $part_export->exporttype eq 'sqlradius' ) { + $username = $svc_x->username; + } elsif ( $part_export->exporttype eq 'sqlradius_withdomain' ) { + $username = $svc_x->email; + } else { + die 'unknown exporttype '. $part_export->exporttype; + } + + my $sth = $dbh->prepare("SELECT SUM($attrib) + FROM radacct + WHERE UserName = ? + AND $str2time AcctStopTime ) >= ? + AND $str2time AcctStopTime ) < ? + AND AcctStopTime IS NOT NULL" + ) or die $dbh->errstr; + $sth->execute($username, $start, $end) or die $sth->errstr; + + $sum += $sth->fetchrow_arrayref->[0]; + + } + + $sum; + +} + +=item get_session_history_sqlradacct TIMESTAMP_START TIMESTAMP_END + +See L<FS::svc_acct/get_session_history_sqlradacct>. Equivalent to +$cust_svc->svc_x->get_session_history_sqlradacct, but more efficient. +Meaningless for records where B<svcdb> is not "svc_acct". + +=cut + +sub get_session_history { + my($self, $start, $end, $attrib) = @_; + + my $username = $self->svc_x->username; + + my @part_export = $self->part_svc->part_export('sqlradius') + or die "no sqlradius export configured for this service type"; + #or return undef; + + my @sessions = (); + + foreach my $part_export ( @part_export ) { + + my $dbh = DBI->connect( map { $part_export->option($_) } + qw(datasrc username password) ) + or die "can't connect to sqlradius database: ". $DBI::errstr; + + #select a unix time conversion function based on database type + my $str2time; + if ( $dbh->{Driver}->{Name} eq 'mysql' ) { + $str2time = 'UNIX_TIMESTAMP('; + } elsif ( $dbh->{Driver}->{Name} eq 'Pg' ) { + $str2time = 'EXTRACT( EPOCH FROM '; + } else { + warn "warning: unknown database type ". $dbh->{Driver}->{Name}. + "; guessing how to convert to UNIX timestamps"; + $str2time = 'extract(epoch from '; + } + + my @fields = qw( acctstarttime acctstoptime acctsessiontime + acctinputoctets acctoutputoctets framedipaddress ); + + my $sth = $dbh->prepare('SELECT '. join(', ', @fields). + " FROM radacct + WHERE UserName = ? + AND $str2time AcctStopTime ) >= ? + AND $str2time AcctStopTime ) <= ? + ORDER BY AcctStartTime DESC + ") or die $dbh->errstr; + $sth->execute($username, $start, $end) or die $sth->errstr; + + push @sessions, map { { %$_ } } @{ $sth->fetchall_arrayref({}) }; + + } + \@sessions + +} + +=back =head1 BUGS @@ -356,6 +602,9 @@ pkg_svc records are not checked in general (here). Deleting this record doesn't check or delete the svc_* record associated with this record. +In seconds_since_sqlradacct, specifying a DATASRC/USERNAME/PASSWORD instead of +a DBI database handle is not yet implemented. + =head1 SEE ALSO L<FS::Record>, L<FS::cust_pkg>, L<FS::part_svc>, L<FS::pkg_svc>, diff --git a/FS/FS/cust_tax_exempt.pm b/FS/FS/cust_tax_exempt.pm index ab873c0a7..da0de000a 100644 --- a/FS/FS/cust_tax_exempt.pm +++ b/FS/FS/cust_tax_exempt.pm @@ -111,6 +111,7 @@ sub check { || $self->ut_number('year') #check better || $self->ut_number('month') #check better || $self->ut_money('amount') + || $self->SUPER::check ; } diff --git a/FS/FS/domain_record.pm b/FS/FS/domain_record.pm index 37cc6c9e8..ea0c48d4f 100644 --- a/FS/FS/domain_record.pm +++ b/FS/FS/domain_record.pm @@ -241,7 +241,7 @@ sub check { if ( $self->rectype eq 'SOA' ) { my $recdata = $self->recdata; $recdata =~ s/\s+/ /g; - $recdata =~ /^([a-z0-9\.\-]+ [\w\-\+]+\.[a-z0-9\.\-]+ \( (\d+ ){5}\))$/i + $recdata =~ /^([a-z0-9\.\-]+ [\w\-\+]+\.[a-z0-9\.\-]+ \( ((\d+|((\d+[WDHMS])+)) ){5}\))$/i or return "Illegal data for SOA record: $recdata"; $self->recdata($1); } elsif ( $self->rectype eq 'NS' ) { @@ -261,7 +261,7 @@ sub check { or return "Illegal data for PTR record: ". $self->recdata; $self->recdata($1); } elsif ( $self->rectype eq 'CNAME' ) { - $self->recdata =~ /^([a-z0-9\.\-]+)$/i + $self->recdata =~ /^([a-z0-9\.\-]+|\@)$/i or return "Illegal data for CNAME record: ". $self->recdata; $self->recdata($1); } elsif ( $self->rectype eq '_mstr' ) { @@ -271,7 +271,7 @@ sub check { die "ack!"; } - ''; #no error + $self->SUPER::check; } =item increment_serial @@ -309,11 +309,30 @@ sub svc_domain { qsearchs('svc_domain', { svcnum => $self->svcnum } ); } +=item zone + +Returns the canonical zone name. + +=cut + +sub zone { + my $self = shift; + my $zone = $self->reczone; # or die ? + if ( $zone =~ /\.$/ ) { + $zone =~ s/\.$//; + } else { + my $svc_domain = $self->svc_domain; # or die ? + $zone .= '.'. $svc_domain->domain; + $zone =~ s/^\@\.//; + } + $zone; +} + =back =head1 VERSION -$Id: domain_record.pm,v 1.11 2002-06-23 19:16:45 ivan Exp $ +$Id: domain_record.pm,v 1.16 2003-08-05 00:20:43 khoff Exp $ =head1 BUGS diff --git a/FS/FS/export_svc.pm b/FS/FS/export_svc.pm index da9ac698a..c104e4538 100644 --- a/FS/FS/export_svc.pm +++ b/FS/FS/export_svc.pm @@ -105,6 +105,7 @@ sub check { || $self->ut_foreign_key('exportnum', 'part_export', 'exportnum') || $self->ut_number('svcpart') || $self->ut_foreign_key('svcpart', 'part_svc', 'svcpart') + || $self->SUPER::check ; } diff --git a/FS/FS/msgcat.pm b/FS/FS/msgcat.pm index fa10d34fa..855b8b291 100644 --- a/FS/FS/msgcat.pm +++ b/FS/FS/msgcat.pm @@ -113,7 +113,7 @@ sub check { $self->locale =~ /^([\w\@]+)$/ or return "illegal locale: ". $self->locale; $self->locale($1); - ''; #no error + $self->SUPER::check } =back diff --git a/FS/FS/nas.pm b/FS/FS/nas.pm index 58c6827ea..2d17df899 100644 --- a/FS/FS/nas.pm +++ b/FS/FS/nas.pm @@ -114,7 +114,9 @@ sub check { || $self->ut_text('nas') || $self->ut_ip('nasip') || $self->ut_domain('nasfqdn') - || $self->ut_numbern('last'); + || $self->ut_numbern('last') + || $self->SUPER::check + ; } =item heartbeat TIMESTAMP @@ -136,7 +138,7 @@ sub heartbeat { =head1 VERSION -$Id: nas.pm,v 1.6 2002-03-04 12:48:49 ivan Exp $ +$Id: nas.pm,v 1.7 2003-08-05 00:20:43 khoff Exp $ =head1 BUGS diff --git a/FS/FS/part_bill_event.pm b/FS/FS/part_bill_event.pm index a31b09b36..86f929424 100644 --- a/FS/FS/part_bill_event.pm +++ b/FS/FS/part_bill_event.pm @@ -37,7 +37,7 @@ FS::Record. The following fields are currently supported: =item eventpart - primary key -=item payby - CARD, BILL, or COMP +=item payby - CARD, DCRD, CHEK, DCHK, LECB, BILL, or COMP =item event - event name @@ -124,7 +124,7 @@ sub check { $c =~ /^\s*\$cust_main\->(suspend|cancel|invoicing_list_addpost|bill|collect)\(\);\s*("";)?\s*$/ - or $c =~ /^\s*\$cust_bill\->(comp|realtime_card|realtime_card_cybercash|batch_card|send)\(\);\s*$/ + or $c =~ /^\s*\$cust_bill\->(comp|realtime_(card|ach|lec)|realtime_card_cybercash|batch_card|send)\(\);\s*$/ or $c =~ /^\s*\$cust_bill\->send\(\'\w+\'\);\s*$/ @@ -140,7 +140,7 @@ sub check { } my $error = $self->ut_numbern('eventpart') - || $self->ut_enum('payby', [qw( CARD BILL COMP )] ) + || $self->ut_enum('payby', [qw( CARD DCRD CHEK DCHK LECB BILL COMP )] ) || $self->ut_text('event') || $self->ut_anything('eventcode') || $self->ut_number('seconds') @@ -160,10 +160,15 @@ sub check { join("\n", $conf->config('invoice_template') ) ); } + unless ( $conf->exists("invoice_latex_$name") ) { + $conf->set( + "invoice_latex_$name" => + join("\n", $conf->config('invoice_latex') ) + ); + } } - ''; - + $self->SUPER::check; } =back diff --git a/FS/FS/part_export.pm b/FS/FS/part_export.pm index 4f45fbeec..8423da299 100644 --- a/FS/FS/part_export.pm +++ b/FS/FS/part_export.pm @@ -274,12 +274,6 @@ sub check { ; return $error if $error; - warn $self->machine. "!!!\n"; - - $self->machine =~ /^([\w\-\.]*)$/ - or return "Illegal machine: ". $self->machine; - $self->machine($1); - $self->nodomain =~ /^(Y?)$/ or return "Illegal nodomain: ". $self->nodomain; $self->nodomain($1); @@ -287,7 +281,7 @@ sub check { #check exporttype? - ''; #no error + $self->SUPER::check; } #=item part_svc @@ -307,6 +301,30 @@ sub part_svc { #confess "FS::part_export::part_svc deprecated"; } +=item svc_x + +Returns a list of associated FS::svc_* records. + +=cut + +sub svc_x { + my $self = shift; + map { $_->svc_x } $self->cust_svc; +} + +=item cust_svc + +Returns a list of associated FS::cust_svc records. + +=cut + +sub cust_svc { + my $self = shift; + map { qsearch('cust_svc', { 'svcpart' => $_->svcpart } ) } + grep { qsearch('cust_svc', { 'svcpart' => $_->svcpart } ) } + $self->export_svc; +} + =item export_svc Returns a list of associated FS::export_svc records. @@ -450,18 +468,22 @@ sub _export_delete { return "_export_delete: unknown export type ". $self->exporttype; } -#fallbacks providing null operations +#call svcdb-specific fallbacks sub _export_suspend { my $self = shift; #warn "warning: _export_suspened unimplemented for". ref($self); - ''; + my $svc_x = shift; + my $new = $svc_x->clone_suspended; + $self->_export_replace( $new, $svc_x ); } sub _export_unsuspend { my $self = shift; #warn "warning: _export_unsuspend unimplemented for ". ref($self); - ''; + my $svc_x = shift; + my $old = $svc_x->clone_kludge_unsuspend; + $self->_export_replace( $svc_x, $old ); } =back @@ -526,7 +548,7 @@ tie my %shellcommands_options, 'Tie::IxHash', #'machine' => { label=>'Remote machine' }, 'user' => { label=>'Remote username', default=>'root' }, 'useradd' => { label=>'Insert command', - default=>'useradd -d $dir -m -s $shell -u $uid -p $crypt_password $username' + default=>'useradd -c $finger -d $dir -m -s $shell -u $uid -p $crypt_password $username' #default=>'cp -pr /etc/skel $dir; chown -R $uid.$gid $dir' }, 'useradd_stdin' => { label=>'Insert command STDIN', @@ -542,7 +564,7 @@ tie my %shellcommands_options, 'Tie::IxHash', default=>'', }, 'usermod' => { label=>'Modify command', - default=>'usermod -d $new_dir -m -l $new_username -s $new_shell -u $new_uid -p $new_crypt_password $old_username', + default=>'usermod -c $new_finger -d $new_dir -m -l $new_username -s $new_shell -u $new_uid -p $new_crypt_password $old_username', #default=>'[ -d $old_dir ] && mv $old_dir $new_dir || ( '. # 'chmod u+t $old_dir; mkdir $new_dir; cd $old_dir; '. # 'find . -depth -print | cpio -pdm $new_dir; '. @@ -554,6 +576,21 @@ tie my %shellcommands_options, 'Tie::IxHash', type =>'textarea', default=>'', }, + 'usermod_pwonly' => { label=>'Disallow username changes', + type =>'checkbox', + }, + 'suspend' => { label=>'Suspension command', + default=>'usermod -L $username', + }, + 'suspend_stdin' => { label=>'Suspension command STDIN', + default=>'', + }, + 'unsuspend' => { label=>'Unsuspension command', + default=>'usermod -U $username', + }, + 'unsuspend_stdin' => { label=>'Unsuspension command STDIN', + default=>'', + }, ; tie my %shellcommands_withdomain_options, 'Tie::IxHash', @@ -579,10 +616,25 @@ tie my %shellcommands_withdomain_options, 'Tie::IxHash', type =>'textarea', #default=>"$_password\n$_password\n", }, + 'usermod_pwonly' => { label=>'Disallow username changes', + type =>'checkbox', + }, + 'suspend' => { label=>'Suspension command', + default=>'', + }, + 'suspend_stdin' => { label=>'Suspension command STDIN', + default=>'', + }, + 'unsuspend' => { label=>'Unsuspension command', + default=>'', + }, + 'unsuspend_stdin' => { label=>'Unsuspension command STDIN', + default=>'', + }, ; tie my %www_shellcommands_options, 'Tie::IxHash', - 'user' => { lable=>'Remote username', default=>'root' }, + 'user' => { label=>'Remote username', default=>'root' }, 'useradd' => { label=>'Insert command', default=>'mkdir /var/www/$zone; chown $username /var/www/$zone; ln -s /var/www/$zone $homedir/$zone', }, @@ -594,6 +646,53 @@ tie my %www_shellcommands_options, 'Tie::IxHash', }, ; +tie my %apache_options, 'Tie::IxHash', + 'user' => { label=>'Remote username', default=>'root' }, + 'httpd_conf' => { label=>'httpd.conf snippet location', + default=>'/etc/apache/httpd-freeside.conf', }, + 'template' => { + label => 'Template', + type => 'textarea', + default => <<'END', +<VirtualHost $domain> #generic +#<VirtualHost ip.addr> #preferred, http://httpd.apache.org/docs/dns-caveats.html +DocumentRoot /var/www/$zone +ServerName $zone +ServerAlias *.$zone +#BandWidthModule On +#LargeFileLimit 4096 12288 +</VirtualHost> + +END + }, +; + +tie my %router_options, 'Tie::IxHash', + 'protocol' => { + label=>'Protocol', + type =>'select', + options => [qw(telnet ssh)], + default => 'telnet'}, + 'insert' => {label=>'Insert command', default=>'' }, + 'delete' => {label=>'Delete command', default=>'' }, + 'replace' => {label=>'Replace command', default=>'' }, + 'Timeout' => {label=>'Time to wait for prompt', default=>'20' }, + 'Prompt' => {label=>'Prompt string', default=>'#' } +; + +tie my %domain_shellcommands_options, 'Tie::IxHash', + 'user' => { label=>'Remote username', default=>'root' }, + 'useradd' => { label=>'Insert command', + default=>'', + }, + 'userdel' => { label=>'Delete command', + default=>'', + }, + 'usermod' => { label=>'Modify command', + default=>'', + }, +; + tie my %textradius_options, 'Tie::IxHash', 'user' => { label=>'Remote username', default=>'root' }, 'users' => { label=>'users file location', default=>'/etc/raddb/users' }, @@ -603,6 +702,20 @@ tie my %sqlradius_options, 'Tie::IxHash', 'datasrc' => { label=>'DBI data source ' }, 'username' => { label=>'Database username' }, 'password' => { label=>'Database password' }, + 'ignore_accounting' => { + type => 'checkbox', + label=>'Ignore accounting records from this database' + }, +; + +tie my %sqlradius_withdomain_options, 'Tie::IxHash', + 'datasrc' => { label=>'DBI data source ' }, + 'username' => { label=>'Database username' }, + 'password' => { label=>'Database password' }, + 'ignore_accounting' => { + type => 'checkbox', + label=>'Ignore accounting records from this database' + }, ; tie my %cyrus_options, 'Tie::IxHash', @@ -612,7 +725,6 @@ tie my %cyrus_options, 'Tie::IxHash', ; tie my %cp_options, 'Tie::IxHash', - 'host' => { label=>'Hostname' }, 'port' => { label=>'Port number' }, 'username' => { label=>'Username' }, 'password' => { label=>'Password' }, @@ -628,25 +740,75 @@ tie my %infostreet_options, 'Tie::IxHash', ; tie my %vpopmail_options, 'Tie::IxHash', - 'machine' => { label=>'vpopmail machine', }, + #'machine' => { label=>'vpopmail machine', }, 'dir' => { label=>'directory', }, # ?more info? default? 'uid' => { label=>'vpopmail uid' }, 'gid' => { label=>'vpopmail gid' }, + 'restart' => { label=> 'vpopmail restart command', + default=> 'cd /home/vpopmail/domains; for domain in *; do /home/vpopmail/bin/vmkpasswd $domain; done; /var/qmail/bin/qmail-newu; killall -HUP qmail-send', + }, +; + +tie my %communigate_pro_options, 'Tie::IxHash', + 'port' => { label=>'Port number', default=>'106', }, + 'login' => { label=>'The administrator account name. The name can contain a domain part.', }, + 'password' => { label=>'The administrator account password.', }, + 'accountType' => { label=>'Type for newly-created accounts', + type=>'select', + options=>[qw( MultiMailbox TextMailbox MailDirMailbox )], + default=>'MultiMailbox', + }, + 'externalFlag' => { label=> 'Create accounts with an external (visible for legacy mailers) INBOX.', + type=>'checkbox', + }, + 'AccessModes' => { label=>'Access modes', + default=>'Mail POP IMAP PWD WebMail WebSite', + }, +; + +tie my %communigate_pro_singledomain_options, 'Tie::IxHash', + 'port' => { label=>'Port number', default=>'106', }, + 'login' => { label=>'The administrator account name. The name can contain a domain part.', }, + 'password' => { label=>'The administrator account password.', }, + 'domain' => { label=>'Domain', }, + 'accountType' => { label=>'Type for newly-created accounts', + type=>'select', + options=>[qw( MultiMailbox TextMailbox MailDirMailbox )], + default=>'MultiMailbox', + }, + 'externalFlag' => { label=> 'Create accounts with an external (visible for legacy mailers) INBOX.', + type=>'checkbox', + }, + 'AccessModes' => { label=>'Access modes', + default=>'Mail POP IMAP PWD WebMail WebSite', + }, ; tie my %bind_options, 'Tie::IxHash', - #'machine' => { label=>'named machine' }, - 'named_conf' => { label => 'named.conf location', - default=> '/etc/bind/named.conf' }, - 'zonepath' => { label => 'path to zone files', - default=> '/etc/bind/', }, + #'machine' => { label=>'named machine' }, + 'named_conf' => { label => 'named.conf location', + default=> '/etc/bind/named.conf' }, + 'zonepath' => { label => 'path to zone files', + default=> '/etc/bind/', }, + 'bind_release' => { label => 'ISC BIND Release', + type => 'select', + options => [qw(BIND8 BIND9)], + default => 'BIND8' }, + 'bind9_minttl' => { label => 'The minttl required by bind9 and RFC1035.', + default => '1D' }, ; tie my %bind_slave_options, 'Tie::IxHash', - #'machine' => { label=> 'Slave machine' }, - 'master' => { label=> 'Master IP address(s) (semicolon-separated)' }, - 'named_conf' => { label => 'named.conf location', - default => '/etc/bind/named.conf' }, + #'machine' => { label=> 'Slave machine' }, + 'master' => { label=> 'Master IP address(s) (semicolon-separated)' }, + 'named_conf' => { label => 'named.conf location', + default => '/etc/bind/named.conf' }, + 'bind_release' => { label => 'ISC BIND Release', + type => 'select', + options => [qw(BIND8 BIND9)], + default => 'BIND8' }, + 'bind9_minttl' => { label => 'The minttl required by bind9 and RFC1035.', + default => '1D' }, ; tie my %http_options, 'Tie::IxHash', @@ -681,11 +843,76 @@ tie my %http_options, 'Tie::IxHash', ; tie my %sqlmail_options, 'Tie::IxHash', - 'datasrc' => { label=>'DBI data source' }, - 'username' => { label=>'Database username' }, - 'password' => { label=>'Database password' }, + 'datasrc' => { label => 'DBI data source' }, + 'username' => { label => 'Database username' }, + 'password' => { label => 'Database password' }, + 'server_type' => { + label => 'Server type', + type => 'select', + options => [qw(dovecot_plain dovecot_crypt dovecot_digest_md5 courier_plain + courier_crypt)], + default => ['dovecot_plain'], }, + 'svc_acct_table' => { label => 'User Table', default => 'user_acct' }, + 'svc_forward_table' => { label => 'Forward Table', default => 'forward' }, + 'svc_domain_table' => { label => 'Domain Table', default => 'domain' }, + 'svc_acct_fields' => { label => 'svc_acct Export Fields', + default => 'username _password domsvc svcnum' }, + 'svc_forward_fields' => { label => 'svc_forward Export Fields', + default => 'domain svcnum catchall' }, + 'svc_domain_fields' => { label => 'svc_domain Export Fields', + default => 'srcsvc dstsvc dst' }, + 'resolve_dstsvc' => { label => q{Resolve svc_forward.dstsvc to an email address and store it in dst. (Doesn't require that you also export dstsvc.)}, + type => 'checkbox' }, + +; + +tie my %ldap_options, 'Tie::IxHash', + 'dn' => { label=>'Root DN' }, + 'password' => { label=>'Root DN password' }, + 'userdn' => { label=>'User DN' }, + 'attributes' => { label=>'Attributes', + type=>'textarea', + default=>join("\n", + 'uid $username', + 'mail $username\@$domain', + 'uidno $uid', + 'gidno $gid', + 'cn $first', + 'sn $last', + 'mailquota $quota', + 'vmail', + 'location', + 'mailtag', + 'mailhost', + 'mailmessagestore $dir', + 'userpassword $crypt_password', + 'hint', + 'answer $sec_phrase', + 'objectclass top,person,inetOrgPerson', + ), + }, + 'radius' => { label=>'Export RADIUS attributes', type=>'checkbox', }, +; + +tie my %forward_shellcommands_options, 'Tie::IxHash', + 'user' => { label=>'Remote username', default=>'root' }, + 'useradd' => { label=>'Insert command', + default=>'', + }, + 'userdel' => { label=>'Delete command', + default=>'', + }, + 'usermod' => { label=>'Modify command', + default=>'', + }, ; +tie my %postfix_options, 'Tie::IxHash', + 'user' => { label=>'Remote username', default=>'root' }, + 'aliases' => { label=>'aliases file location', default=>'/etc/aliases' }, + 'virtual' => { label=>'virtual file location', default=>'/etc/postfix/virtual' }, + 'mydomain' => { label=>'local domain', default=>'' }, +; #export names cannot have dashes... %exports = ( @@ -719,26 +946,39 @@ tie my %sqlmail_options, 'Tie::IxHash', 'desc' => 'Real-time export via remote SSH (i.e. useradd, userdel, etc.)', 'options' => \%shellcommands_options, 'nodomain' => 'Y', - 'notes' => 'Run remote commands via SSH. Usernames are considered unique (also see shellcommands_withdomain). You probably want this if the commands you are running will not accept a domain as a parameter. You will need to <a href="../docs/ssh.html">setup SSH for unattended operation</a>.<BR><BR>Use these buttons for some useful presets:<UL><LI><INPUT TYPE="button" VALUE="Linux/NetBSD" onClick=\'this.form.useradd.value = "useradd -c $finger -d $dir -m -s $shell -u $uid -p $crypt_password $username"; this.form.useradd_stdin.value = ""; this.form.userdel.value = "userdel -r $username"; this.form.userdel_stdin.value=""; this.form.usermod.value = "usermod -c $new_finger -d $new_dir -m -l $new_username -s $new_shell -u $new_uid -p $new_crypt_password $old_username"; this.form.usermod_stdin.value = "";\'><LI><INPUT TYPE="button" VALUE="FreeBSD" onClick=\'this.form.useradd.value = "pw useradd $username -d $dir -m -s $shell -u $uid -c $finger -h 0"; this.form.useradd_stdin.value = "$_password\n"; this.form.userdel.value = "pw userdel $username -r"; this.form.userdel_stdin.value=""; this.form.usermod.value = "pw usermod $old_username -d $new_dir -m -l $new_username -s $new_shell -u $new_uid -c $new_finger -h 0"; this.form.usermod_stdin.value = "$new__password\n";\'><LI><INPUT TYPE="button" VALUE="Just maintain directories (use with sysvshell or bsdshell)" onClick=\'this.form.useradd.value = "cp -pr /etc/skel $dir; chown -R $uid.$gid $dir"; this.form.useradd_stdin.value = ""; this.form.usermod.value = "[ -d $old_dir ] && mv $old_dir $new_dir || ( chmod u+t $old_dir; mkdir $new_dir; cd $old_dir; find . -depth -print | cpio -pdm $new_dir; chmod u-t $new_dir; chown -R $uid.$gid $new_dir; rm -rf $old_dir )"; this.form.usermod_stdin.value = ""; this.form.userdel.value = "rm -rf $dir"; this.form.userdel_stdin.value="";\'></UL>', + 'notes' => 'Run remote commands via SSH. Usernames are considered unique (also see shellcommands_withdomain). You probably want this if the commands you are running will not accept a domain as a parameter. You will need to <a href="../docs/ssh.html">setup SSH for unattended operation</a>.<BR><BR>Use these buttons for some useful presets:<UL><LI><INPUT TYPE="button" VALUE="Linux" onClick=\'this.form.useradd.value = "useradd -c $finger -d $dir -m -s $shell -u $uid -p $crypt_password $username"; this.form.useradd_stdin.value = ""; this.form.userdel.value = "userdel -r $username"; this.form.userdel_stdin.value=""; this.form.usermod.value = "usermod -c $new_finger -d $new_dir -m -l $new_username -s $new_shell -u $new_uid -p $new_crypt_password $old_username"; this.form.usermod_stdin.value = ""; this.form.suspend.value = "usermod -L $username"; this.form.suspend_stdin.value=""; this.form.unsuspend.value = "usermod -U $username"; this.form.unsuspend_stdin.value="";\'><LI><INPUT TYPE="button" VALUE="FreeBSD" onClick=\'this.form.useradd.value = "lockf /etc/passwd.lock pw useradd $username -d $dir -m -s $shell -u $uid -g $gid -c $finger -h 0"; this.form.useradd_stdin.value = "$_password\n"; this.form.userdel.value = "lockf /etc/passwd.lock pw userdel $username -r"; this.form.userdel_stdin.value=""; this.form.usermod.value = "lockf /etc/passwd.lock pw usermod $old_username -d $new_dir -m -l $new_username -s $new_shell -u $new_uid -c $new_finger -h 0"; this.form.usermod_stdin.value = "$new__password\n"; this.form.suspend.value = "lockf /etc/passwd.lock pw lock $username"; this.form.suspend_stdin.value=""; this.form.unsuspend.value = "lockf /etc/passwd.lock pw unlock $username"; this.form.unsuspend_stdin.value="";\'> Note: On FreeBSD, due to deficient locking in pw(1), you must disable the chpass(1), chsh(1), chfn(1), passwd(1), and vipw(1) commands, or replace them with wrappers that prepend "lockf /etc/passwd.lock". Alternatively, apply the patch in <A HREF="http://www.freebsd.org/cgi/query-pr.cgi?pr=23501">FreeBSD PR#23501</A> and remove the "lockf /etc/passwd.lock" from these default commands.<LI><INPUT TYPE="button" VALUE="NetBSD/OpenBSD" onClick=\'this.form.useradd.value = "useradd -c $finger -d $dir -m -s $shell -u $uid -p $crypt_password $username"; this.form.useradd_stdin.value = ""; this.form.userdel.value = "userdel -r $username"; this.form.userdel_stdin.value=""; this.form.usermod.value = "usermod -c $new_finger -d $new_dir -m -l $new_username -s $new_shell -u $new_uid -p $new_crypt_password $old_username"; this.form.usermod_stdin.value = ""; this.form.suspend.value = ""; this.form.suspend_stdin.value=""; this.form.unsuspend.value = ""; this.form.unsuspend_stdin.value="";\'><LI><INPUT TYPE="button" VALUE="Just maintain directories (use with sysvshell or bsdshell)" onClick=\'this.form.useradd.value = "cp -pr /etc/skel $dir; chown -R $uid.$gid $dir"; this.form.useradd_stdin.value = ""; this.form.usermod.value = "[ -d $old_dir ] && mv $old_dir $new_dir || ( chmod u+t $old_dir; mkdir $new_dir; cd $old_dir; find . -depth -print | cpio -pdm $new_dir; chmod u-t $new_dir; chown -R $new_uid.$new_gid $new_dir; rm -rf $old_dir )"; this.form.usermod_stdin.value = ""; this.form.userdel.value = "rm -rf $dir"; this.form.userdel_stdin.value=""; this.form.suspend.value = ""; this.form.suspend_stdin.value=""; this.form.unsuspend.value = ""; this.form.unsuspend_stdin.value="";\'></UL>The following variables are available for interpolation (prefixed with new_ or old_ for replace operations): <UL><LI><code>$username</code><LI><code>$_password</code><LI><code>$quoted_password</code> - unencrypted password quoted for the shell<LI><code>$crypt_password</code> - encrypted password<LI><code>$uid</code><LI><code>$gid</code><LI><code>$finger</code> - GECOS, already quoted for the shell (do not add additional quotes)<LI><code>$dir</code> - home directory<LI><code>$shell</code><LI><code>$quota</code><LI>All other fields in <a href="../docs/schema.html#svc_acct">svc_acct</a> are also available.</UL>', }, 'shellcommands_withdomain' => { - 'desc' => 'Real-time export via remote SSH.', + 'desc' => 'Real-time export via remote SSH (vpopmail, etc.).', 'options' => \%shellcommands_withdomain_options, - 'notes' => 'Run remote commands via SSH. username@domain (rather than just usernames) are considered unique (also see shellcommands). You probably want this if the commands you are running will accept a domain as a parameter, and will allow the same username with different domains. You will need to <a href="../docs/ssh.html">setup SSH for unattended operation</a>.', + 'notes' => 'Run remote commands via SSH. username@domain (rather than just usernames) are considered unique (also see shellcommands). You probably want this if the commands you are running will accept a domain as a parameter, and will allow the same username with different domains. You will need to <a href="../docs/ssh.html">setup SSH for unattended operation</a>.<BR><BR>Use these buttons for some useful presets:<UL><LI><INPUT TYPE="button" VALUE="vpopmail" onClick=\'this.form.useradd.value = "/home/vpopmail/bin/vadduser $username\\\@$domain $quoted_password"; this.form.useradd_stdin.value = ""; this.form.userdel.value = "/home/vpopmail/bin/vdeluser $username\\\@$domain"; this.form.userdel_stdin.value=""; this.form.usermod.value = "/home/vpopmail/bin/vpasswd $new_username\\\@$new_domain $new_quoted_password"; this.form.usermod_stdin.value = ""; this.form.usermod_pwonly.checked = true;\'></UL>The following variables are available for interpolation (prefixed with <code>new_</code> or <code>old_</code> for replace operations): <UL><LI><code>$username</code><LI><code>$domain</code><LI><code>$_password</code><LI><code>$quoted_password</code> - unencrypted password quoted for the shell<LI><code>$crypt_password</code> - encrypted password<LI><code>$uid</code><LI><code>$gid</code><LI><code>$finger</code> - GECOS, already quoted for the shell (do not add additional quotes)<LI><code>$dir</code> - home directory<LI><code>$shell</code><LI><code>$quota</code><LI>All other fields in <a href="../docs/schema.html#svc_acct">svc_acct</a> are also available.</UL>', + }, + + 'ldap' => { + 'desc' => 'Real-time export to LDAP', + 'options' => \%ldap_options, + 'notes' => 'Real-time export to arbitrary LDAP attributes. Requires installation of <a href="http://search.cpan.org/search?dist=Net-LDAP">Net::LDAP</a> from CPAN.', }, 'sqlradius' => { - 'desc' => 'Real-time export to SQL-backed RADIUS (ICRADIUS, FreeRADIUS)', + 'desc' => 'Real-time export to SQL-backed RADIUS (FreeRADIUS, ICRADIUS, Radiator)', 'options' => \%sqlradius_options, 'nodomain' => 'Y', - 'notes' => 'Real-time export of radcheck, radreply and usergroup tables to any SQL database for <a href="http://www.freeradius.org/">FreeRADIUS</a> or <a href="http://radius.innercite.com/">ICRADIUS</a>. An existing RADIUS database will be updated in realtime, but you can use <a href="../docs/man/bin/freeside-sqlradius-reset">freeside-sqlradius-reset</a> to delete the entire RADIUS database and repopulate the tables from the Freeside database. See the <a href="http://search.cpan.org/doc/TIMB/DBI-1.23/DBI.pm">DBI documentation</a> and the <a href="http://search.cpan.org/search?mode=module&query=DBD%3A%3A">documentation for your DBD</a> for the exact syntax of a DBI data source. If using <a href="http://www.freeradius.org/">FreeRADIUS</a> 0.5 or above, make sure your <b>op</b> fields are set to allow NULL values.', + 'notes' => 'Real-time export of radcheck, radreply and usergroup tables to any SQL database for <a href="http://www.freeradius.org/">FreeRADIUS</a>, <a href="http://radius.innercite.com/">ICRADIUS</a> or <a href="http://www.open.com.au/radiator/">Radiator</a>. This export does not export RADIUS realms (see also sqlradius_withdomain). An existing RADIUS database will be updated in realtime, but you can use <a href="../docs/man/bin/freeside-sqlradius-reset">freeside-sqlradius-reset</a> to delete the entire RADIUS database and repopulate the tables from the Freeside database. See the <a href="http://search.cpan.org/doc/TIMB/DBI/DBI.pm#connect">DBI documentation</a> and the <a href="http://search.cpan.org/search?mode=module&query=DBD%3A%3A">documentation for your DBD</a> for the exact syntax of a DBI data source.<ul><li>Using FreeRADIUS 0.9.0 with the PostgreSQL backend, the db_postgresql.sql schema and postgresql.conf queries contain incompatible changes. This is fixed in 0.9.1. Only new installs with 0.9.0 and PostgreSQL are affected - upgrades and other database backends and versions are unaffected.<li>Using ICRADIUS, add a dummy "op" column to your database: <blockquote><code>ALTER TABLE radcheck ADD COLUMN op VARCHAR(2) NOT NULL DEFAULT \'==\'<br>ALTER TABLE radreply ADD COLUMN op VARCHAR(2) NOT NULL DEFAULT \'==\'<br>ALTER TABLE radgroupcheck ADD COLUMN op VARCHAR(2) NOT NULL DEFAULT \'==\'<br>ALTER TABLE radgroupreply ADD COLUMN op VARCHAR(2) NOT NULL DEFAULT \'==\'</code></blockquote><li>Using Radiator, see the <a href="http://www.open.com.au/radiator/faq.html#38">Radiator FAQ</a> for configuration information.</ul>', + }, + + 'sqlradius_withdomain' => { + 'desc' => 'Real-time export to SQL-backed RADIUS (FreeRADIUS, ICRADIUS, Radiator) with realms', + 'options' => \%sqlradius_withdomain_options, + 'nodomain' => '', + 'notes' => 'Real-time export of radcheck, radreply and usergroup tables to any SQL database for <a href="http://www.freeradius.org/">FreeRADIUS</a>, <a href="http://radius.innercite.com/">ICRADIUS</a> or <a href="http://www.open.com.au/radiator/">Radiator</a>. This export exports domains to RADIUS realms (see also sqlradius). An existing RADIUS database will be updated in realtime, but you can use <a href="../docs/man/bin/freeside-sqlradius-reset">freeside-sqlradius-reset</a> to delete the entire RADIUS database and repopulate the tables from the Freeside database. See the <a href="http://search.cpan.org/doc/TIMB/DBI/DBI.pm#connect">DBI documentation</a> and the <a href="http://search.cpan.org/search?mode=module&query=DBD%3A%3A">documentation for your DBD</a> for the exact syntax of a DBI data source.<ul><li>Using FreeRADIUS 0.9.0 with the PostgreSQL backend, the db_postgresql.sql schema and postgresql.conf queries contain incompatible changes. This is fixed in 0.9.1. Only new installs with 0.9.0 and PostgreSQL are affected - upgrades and other database backends and versions are unaffected.<li>Using ICRADIUS, add a dummy "op" column to your database: <blockquote><code>ALTER TABLE radcheck ADD COLUMN op VARCHAR(2) NOT NULL DEFAULT \'==\'<br>ALTER TABLE radreply ADD COLUMN op VARCHAR(2) NOT NULL DEFAULT \'==\'<br>ALTER TABLE radgroupcheck ADD COLUMN op VARCHAR(2) NOT NULL DEFAULT \'==\'<br>ALTER TABLE radgroupreply ADD COLUMN op VARCHAR(2) NOT NULL DEFAULT \'==\'</code></blockquote><li>Using Radiator, see the <a href="http://www.open.com.au/radiator/faq.html#38">Radiator FAQ</a> for configuration information.</ul>', }, 'sqlmail' => { 'desc' => 'Real-time export to SQL-backed mail server', 'options' => \%sqlmail_options, - 'nodomain' => 'Y', + 'nodomain' => '', 'notes' => 'Database schema can be made to work with Courier IMAP and Exim. Others could work but are untested. (...extended description from pc-intouch?...)', }, @@ -765,7 +1005,20 @@ tie my %sqlmail_options, 'Tie::IxHash', 'vpopmail' => { 'desc' => 'Real-time export to vpopmail text files', 'options' => \%vpopmail_options, - 'notes' => 'Real time export to <a href="http://inter7.com/vpopmail/">vpopmail</a> text files (...extended description from jeff?...)', + 'notes' => 'Real time export to <a href="http://inter7.com/vpopmail/">vpopmail</a> text files. <a href="http://search.cpan.org/search?dist=File-Rsync">File::Rsync</a> must be installed, and you will need to <a href="../docs/ssh.html">setup SSH for unattended operation</a> to <b>vpopmail</b>@<i>export.host</i>.', + }, + + 'communigate_pro' => { + 'desc' => 'Real-time export to a CommuniGate Pro mail server', + 'options' => \%communigate_pro_options, + 'notes' => 'Real time export to a <a href="http://www.stalker.com/CommuniGatePro/">CommuniGate Pro</a> mail server. The <a href="http://www.stalker.com/CGPerl/">CommuniGate Pro Perl Interface</a> must be installed as CGP::CLI.', + }, + + 'communigate_pro_singledomain' => { + 'desc' => 'Real-time export to a CommuniGate Pro mail server, one domain only', + 'options' => \%communigate_pro_singledomain_options, + 'nodomain' => 'Y', + 'notes' => 'Real time export to a <a href="http://www.stalker.com/CommuniGatePro/">CommuniGate Pro</a> mail server. This is an unusual export to CommuniGate Pro that forces all accounts into a single domain. As CommuniGate Pro supports multiple domains, unless you have a specific reason for using this export, you probably want to use the communigate_pro export instead. The <a href="http://www.stalker.com/CGPerl/">CommuniGate Pro Perl Interface</a> must be installed as CGP::CLI.', }, }, @@ -797,27 +1050,61 @@ tie my %sqlmail_options, 'Tie::IxHash', 'notes' => 'Database schema can be made to work with Courier IMAP and Exim. Others could work but are untested. (...extended description from pc-intouch?...)', }, + 'domain_shellcommands' => { + 'desc' => 'Run remote commands via SSH, for domains.', + 'options' => \%domain_shellcommands_options, + 'notes' => 'Run remote commands via SSH, for domains. You will need to <a href="../docs/ssh.html">setup SSH for unattended operation</a>.<BR><BR>Use these buttons for some useful presets:<UL><LI><INPUT TYPE="button" VALUE="qmail catchall .qmail-domain-default maintenance" onClick=\'this.form.useradd.value = "[ \"$uid\" -a \"$gid\" -a \"$dir\" -a \"$qdomain\" ] && [ -e $dir/.qmail-$qdomain-default ] || { touch $dir/.qmail-$qdomain-default; chown $uid:$gid $dir/.qmail-$qdomain-default; }"; this.form.userdel.value = ""; this.form.usermod.value = "";\'></UL>The following variables are available for interpolation (prefixed with <code>new_</code> or <code>old_</code> for replace operations): <UL><LI><code>$domain</code><LI><code>$qdomain</code> - domain with periods replaced by colons<LI><code>$uid</code> - of catchall account<LI><code>$gid</code> - of catchall account<LI><code>$dir</code> - home directory of catchall account<LI>All other fields in <a href="../docs/schema.html#svc_domain">svc_domain</a> are also available.</UL>', + }, + }, - 'svc_acct_sm' => {}, - 'svc_forward' => { 'sqlmail' => { 'desc' => 'Real-time export to SQL-backed mail server', 'options' => \%sqlmail_options, #'nodomain' => 'Y', - 'notes' => 'Database schema can be made to work with Courier IMAP and Exim. Others could work but are untested. (...extended description from pc-intouch?...)', + 'notes' => 'Database schema can be made to work with Courier IMAP and Exim. Others could work but are untested. (...extended description from fire2wire?...)', }, + + 'forward_shellcommands' => { + 'desc' => 'Run remote commands via SSH, for forwards', + 'options' => \%forward_shellcommands_options, + 'notes' => 'Run remote commands via SSH, for forwards. You will need to <a href="../docs/ssh.html">setup SSH for unattended operation</a>.<BR><BR>Use these buttons for some useful presets:<UL><LI><INPUT TYPE="button" VALUE="text vpopmail maintenance" onClick=\'this.form.useradd.value = "[ -d /home/vpopmail/domains/$domain/$username ] && { echo \"$destination\" > /home/vpopmail/domains/$domain/$username/.qmail; chown vpopmail:vchkpw /home/vpopmail/domains/$domain/$username/.qmail; }"; this.form.userdel.value = "rm /home/vpopmail/domains/$domain/$username/.qmail"; this.form.usermod.value = "mv /home/vpopmail/domains/$old_domain/$old_username/.qmail /home/vpopmail/domains/$new_domain/$new_username; [ \"$old_destination\" != \"$new_destination\" ] && { echo \"$new_destination\" > /home/vpopmail/domains/$new_domain/$new_username/.qmail; chown vpopmail:vchkpw /home/vpopmail/domains/$new_domain/$new_username/.qmail; }";\'></UL>The following variables are available for interpolation (prefixed with <code>new_</code> or <code>old_</code> for replace operations): <UL><LI><code>$username</code><LI><code>$domain</code><LI><code>$destination</code> - forward destination<LI>All other fields in <a href="../docs/schema.html#svc_forward">svc_forward</a> are also available.</UL>', + }, + + 'postfix' => { + 'desc' => 'Real-time export to Postfix text files', + 'options' => \%postfix_options, + #'nodomain' => 'Y', + 'notes' => 'Batch export of Postfix aliases and virtual files. <a href="http://search.cpan.org/search?dist=File-Rsync">File::Rsync</a> must be installed. Run bin/postfix.export to export the files.', + }, + }, 'svc_www' => { 'www_shellcommands' => { 'desc' => 'Run remote commands via SSH, for virtual web sites.', 'options' => \%www_shellcommands_options, - 'notes' => 'Run remote commands via SSH, for virtual web sites. You will need to <a href="../docs/ssh.html">setup SSH for unattended operation</a>.', + 'notes' => 'Run remote commands via SSH, for virtual web sites. You will need to <a href="../docs/ssh.html">setup SSH for unattended operation</a>.<BR><BR>The following variables are available for interpolation (prefixed with <code>new_</code> or <code>old_</code> for replace operations): <UL><LI><code>$zone</code><LI><code>$username</code><LI><code>$homedir</code><LI>All other fields in <a href="../docs/schema.html#svc_www">svc_www</a> are also available.</UL>', }, + 'apache' => { + 'desc' => 'Export an Apache httpd.conf file snippet.', + 'options' => \%apache_options, + 'notes' => 'Batch export of an httpd.conf snippet from a template. Typically used with something like <code>Include /etc/apache/httpd-freeside.conf</code> in httpd.conf. <a href="http://search.cpan.org/search?dist=File-Rsync">File::Rsync</a> must be installed. Run bin/apache.export to export the files.', + }, + }, + + 'svc_broadband' => { + 'router' => { + 'desc' => 'Send a command to a router.', + 'options' => \%router_options, + 'notes' => '', + }, + }, + + 'svc_external' => { }, ); diff --git a/FS/FS/part_export/apache.pm b/FS/FS/part_export/apache.pm new file mode 100644 index 000000000..9161d72b3 --- /dev/null +++ b/FS/FS/part_export/apache.pm @@ -0,0 +1,7 @@ +package FS::part_export::apache; + +use vars qw(@ISA); +use FS::part_export::null; + +@ISA = qw(FS::part_export::null); + diff --git a/FS/FS/part_export/communigate_pro.pm b/FS/FS/part_export/communigate_pro.pm new file mode 100644 index 000000000..557aad91d --- /dev/null +++ b/FS/FS/part_export/communigate_pro.pm @@ -0,0 +1,144 @@ +package FS::part_export::communigate_pro; + +use vars qw(@ISA); +use FS::part_export; +use FS::queue; + +@ISA = qw(FS::part_export); + +sub rebless { shift; } + +sub export_username { + my($self, $svc_acct) = (shift, shift); + $svc_acct->email; +} + +sub _export_insert { + my( $self, $svc_acct ) = (shift, shift); + my @options = ( $svc_acct->svcnum, 'CreateAccount', + 'accountName' => $self->export_username($svc_acct), + 'accountType' => $self->option('accountType'), + 'AccessModes' => $self->option('AccessModes'), + 'RealName' => $svc_acct->finger, + 'Password' => $svc_acct->_password, + ); + push @options, 'MaxAccountSize' => $svc_acct->quota if $svc_acct->quota; + push @options, 'externalFlag' => $self->option('externalFlag') + if $self->option('externalFlag'); + + $self->communigate_pro_queue( @options ); +} + +sub _export_replace { + my( $self, $new, $old ) = (shift, shift, shift); + return "can't (yet) change username with CommuniGate Pro" + if $old->username ne $new->username; + return "can't (yet) change domain with CommuniGate Pro" + if $self->export_username($old) ne $self->export_username($new); + return "can't (yet) change GECOS with CommuniGate Pro" + if $old->finger ne $new->finger; + return "can't (yet) change quota with CommuniGate Pro" + if $old->quota ne $new->quota; + return '' unless $old->username ne $new->username + || $old->_password ne $new->_password + || $old->finger ne $new->finger + || $old->quota ne $new->quota; + + return '' if '*SUSPENDED* '. $old->_password eq $new->_password; + + #my $err_or_queue = $self->communigate_pro_queue( $new->svcnum,'RenameAccount', + # $old->email, $new->email ); + #return $err_or_queue unless ref($err_or_queue); + #my $jobnum = $err_or_queue->jobnum; + + $self->communigate_pro_queue( $new->svcnum, 'SetAccountPassword', + $self->export_username($new), $new->_password ) + if $new->_password ne $old->_password; + +} + +sub _export_delete { + my( $self, $svc_acct ) = (shift, shift); + $self->communigate_pro_queue( $svc_acct->svcnum, 'DeleteAccount', + $self->export_username($svc_acct), + ); +} + +sub _export_suspend { + my( $self, $svc_acct ) = (shift, shift); + $self->communigate_pro_queue( $svc_acct->svcnum, 'UpdateAccountSettings', + 'accountName' => $self->export_username($svc_acct), + 'AccessModes' => 'Mail', + ); +} + +sub _export_unsuspend { + my( $self, $svc_acct ) = (shift, shift); + $self->communigate_pro_queue( $svc_acct->svcnum, 'UpdateAccountSettings', + 'accountName' => $self->export_username($svc_acct), + 'AccessModes' => $self->option('AccessModes'), + ); +} + +sub communigate_pro_queue { + my( $self, $svcnum, $method ) = (shift, shift, shift); + my @kludge_methods = qw(CreateAccount UpdateAccountSettings); + my $sub = 'communigate_pro_command'; + $sub = $method if grep { $method eq $_ } @kludge_methods; + my $queue = new FS::queue { + 'svcnum' => $svcnum, + 'job' => "FS::part_export::communigate_pro::$sub", + }; + $queue->insert( + $self->machine, + $self->option('port'), + $self->option('login'), + $self->option('password'), + $method, + @_, + ); + +} + +sub CreateAccount { + my( $machine, $port, $login, $password, $method, %args ) = @_; + my $accountName = delete $args{'accountName'}; + my $accountType = delete $args{'accountType'}; + my $externalFlag = delete $args{'externalFlag'}; + $args{'AccessModes'} = [ split(' ', $args{'AccessModes'}) ]; + my @args = ( accountName => $accountName, + accountType => $accountType, + settings => \%args, + ); + #externalFlag => $externalFlag, + push @args, externalFlag => $externalFlag if $externalFlag; + + communigate_pro_command( $machine, $port, $login, $password, $method, @args ); + +} + +sub UpdateAccountSettings { + my( $machine, $port, $login, $password, $method, %args ) = @_; + my $accountName = delete $args{'accountName'}; + $args{'AccessModes'} = [ split(' ', $args{'AccessModes'}) ]; + @args = ( $accountName, \%args ); + communigate_pro_command( $machine, $port, $login, $password, $method, @args ); +} + +sub communigate_pro_command { #subroutine, not method + my( $machine, $port, $login, $password, $method, @args ) = @_; + + eval "use CGP::CLI"; + + my $cli = new CGP::CLI( { + 'PeerAddr' => $machine, + 'PeerPort' => $port, + 'login' => $login, + 'password' => $password, + } ) or die "Can't login to CGPro: $CGP::ERR_STRING\n"; + + $cli->$method(@args) or die "CGPro error: ". $cli->getErrMessage; + + $cli->Logout or die "Can't logout of CGPro: $CGP::ERR_STRING\n"; + +} diff --git a/FS/FS/part_export/communigate_pro_singledomain.pm b/FS/FS/part_export/communigate_pro_singledomain.pm new file mode 100644 index 000000000..11574af9b --- /dev/null +++ b/FS/FS/part_export/communigate_pro_singledomain.pm @@ -0,0 +1,11 @@ +package FS::part_export::communigate_pro_singledomain; + +use vars qw(@ISA); +use FS::part_export::communigate_pro; + +@ISA = qw(FS::part_export::communigate_pro); + +sub export_username { + my($self, $svc_acct) = (shift, shift); + $svc_acct->username. '@'. $self->option('domain'); +} diff --git a/FS/FS/part_export/cp.pm b/FS/FS/part_export/cp.pm index d998c1d95..c4750dd5d 100644 --- a/FS/FS/part_export/cp.pm +++ b/FS/FS/part_export/cp.pm @@ -10,10 +10,10 @@ sub rebless { shift; } sub _export_insert { my( $self, $svc_acct ) = (shift, shift); $self->cp_queue( $svc_acct->svcnum, 'create_mailbox', - Mailbox => $svc_acct->username, - Password => $svc_acct->_password, - Workgroup => $self->option('workgroup'), - Domain => $svc_acct->domain, + 'Mailbox' => $svc_acct->username, + 'Password' => $svc_acct->_password, + 'Workgroup' => $self->option('workgroup'), + 'Domain' => $svc_acct->domain, ); } @@ -30,8 +30,30 @@ sub _export_replace { sub _export_delete { my( $self, $svc_acct ) = (shift, shift); $self->cp_queue( $svc_acct->svcnum, 'delete_mailbox', - Mailbox => $svc_acct->username, - Domain => $svc_acct->domain, + 'Mailbox' => $svc_acct->username, + 'Domain' => $svc_acct->domain, + ); +} + +sub _export_suspend { + my( $self, $svc_acct ) = (shift, shift); + $self->cp_queue( $svc_acct->svcnum, 'set_mailbox_status', + 'Mailbox' => $svc_acct->username, + 'Domain' => $svc_acct->domain, + 'OTHER' => 'T', + 'OTHER_SUSPEND' => 'T', + ); +} + +sub _export_unsuspend { + my( $self, $svc_acct ) = (shift, shift); + $self->cp_queue( $svc_acct->svcnum, 'set_mailbox_status', + 'Mailbox' => $svc_acct->username, + 'Domain' => $svc_acct->domain, + 'PAYMENT' => 'F', + 'OTHER' => 'F', + 'OTHER_SUSPEND' => 'F', + 'OTHER_BOUNCE' => 'F', ); } @@ -42,7 +64,7 @@ sub cp_queue { 'job' => 'FS::part_export::cp::cp_command', }; $queue->insert( - $self->option('host'), + $self->machine, $self->option('port'), $self->option('username'), $self->option('password'), @@ -69,20 +91,22 @@ sub cp_command { #subroutine, not method ); } - my $other = 'F'; + #my $other = 'F'; if ( $new_password =~ /^\*SUSPENDED\* (.*)$/ ) { $new_password = $1; - $other = 'T'; + # $other = 'T'; } - cp_command($host, $port, $username, $password, 'set_mailbox_status', - Domain => $domain, - Mailbox => $new_username, - Other => $other, - Other_Bounce => $other, - ); + #cp_command($host, $port, $username, $password, $login_domain, + # 'set_mailbox_status', + # Domain => $domain, + # Mailbox => $new_username, + # Other => $other, + # Other_Bounce => $other, + #); if ( $old_password ne $new_password ) { - cp_command($host, $port, $username, $password, 'change_mailbox', + cp_command($host, $port, $username, $password, $login_domain, + 'change_mailbox', Domain => $domain, Mailbox => $new_username, Password => $new_password, diff --git a/FS/FS/part_export/domain_shellcommands.pm b/FS/FS/part_export/domain_shellcommands.pm new file mode 100644 index 000000000..d295eece0 --- /dev/null +++ b/FS/FS/part_export/domain_shellcommands.pm @@ -0,0 +1,110 @@ +package FS::part_export::domain_shellcommands; + +use strict; +use vars qw(@ISA); +use FS::part_export; + +@ISA = qw(FS::part_export); + +sub rebless { shift; } + +sub _export_insert { + my($self) = shift; + $self->_export_command('useradd', @_); +} + +sub _export_delete { + my($self) = shift; + $self->_export_command('userdel', @_); +} + +sub _export_command { + my ( $self, $action, $svc_domain) = (shift, shift, shift); + my $command = $self->option($action); + + #set variable for the command + no strict 'vars'; + { + no strict 'refs'; + ${$_} = $svc_domain->getfield($_) foreach $svc_domain->fields; + } + ( $qdomain = $domain ) =~ s/\./:/g; #see dot-qmail(5): EXTENSION ADDRESSES + + if ( $svc_domain->catchall ) { + no strict 'refs'; + my $svc_acct = $svc_domain->catchall_svc_acct; + ${$_} = $svc_acct->getfield($_) foreach qw(uid gid dir); + } else { + no strict 'refs'; + ${$_} = '' foreach qw(uid gid dir); + } + + #done setting variables for the command + + $self->shellcommands_queue( $svc_domain->svcnum, + user => $self->option('user')||'root', + host => $self->machine, + command => eval(qq("$command")), + ); +} + +sub _export_replace { + my($self, $new, $old ) = (shift, shift, shift); + my $command = $self->option('usermod'); + + #set variable for the command + no strict 'vars'; + { + no strict 'refs'; + ${"old_$_"} = $old->getfield($_) foreach $old->fields; + ${"new_$_"} = $new->getfield($_) foreach $new->fields; + } + ( $old_qdomain = $old_domain ) =~ s/\./:/g; #see dot-qmail(5): EXTENSION ADDRESSES + ( $new_qdomain = $new_domain ) =~ s/\./:/g; #see dot-qmail(5): EXTENSION ADDRESSES + + if ( $old->catchall ) { + no strict 'refs'; + my $svc_acct = $old->catchall_svc_acct; + ${"old_$_"} = $svc_acct->getfield($_) foreach qw(uid gid dir); + } else { + ${"old_$_"} = '' foreach qw(uid gid dir); + } + if ( $new->catchall ) { + no strict 'refs'; + my $svc_acct = $new->catchall_svc_acct; + ${"new_$_"} = $svc_acct->getfield($_) foreach qw(uid gid dir); + } else { + ${"new_$_"} = '' foreach qw(uid gid dir); + } + + #done setting variables for the command + + $self->shellcommands_queue( $new->svcnum, + user => $self->option('user')||'root', + host => $self->machine, + command => eval(qq("$command")), + ); +} + +#a good idea to queue anything that could fail or take any time +sub shellcommands_queue { + my( $self, $svcnum ) = (shift, shift); + my $queue = new FS::queue { + 'svcnum' => $svcnum, + 'job' => "FS::part_export::domain_shellcommands::ssh_cmd", + }; + $queue->insert( @_ ); +} + +sub ssh_cmd { #subroutine, not method + use Net::SSH '0.08'; + &Net::SSH::ssh_cmd( { @_ } ); +} + +#sub shellcommands_insert { #subroutine, not method +#} +#sub shellcommands_replace { #subroutine, not method +#} +#sub shellcommands_delete { #subroutine, not method +#} + diff --git a/FS/FS/part_export/forward_shellcommands.pm b/FS/FS/part_export/forward_shellcommands.pm new file mode 100644 index 000000000..5d3145715 --- /dev/null +++ b/FS/FS/part_export/forward_shellcommands.pm @@ -0,0 +1,110 @@ +package FS::part_export::forward_shellcommands; + +use strict; +use vars qw(@ISA); +use FS::part_export; + +@ISA = qw(FS::part_export); + +sub rebless { shift; } + +sub _export_insert { + my($self) = shift; + $self->_export_command('useradd', @_); +} + +sub _export_delete { + my($self) = shift; + $self->_export_command('userdel', @_); +} + +sub _export_command { + my ( $self, $action, $svc_forward ) = (shift, shift, shift); + my $command = $self->option($action); + + #set variable for the command + no strict 'vars'; + { + no strict 'refs'; + ${$_} = $svc_forward->getfield($_) foreach $svc_forward->fields; + } + + my $svc_acct = $svc_forward->srcsvc_acct; + $username = $svc_acct->username; + $domain = $svc_acct->domain; + if ($svc_forward->dstsvc_acct) { + $destination = $svc_forward->dstsvc_acct->email; + } else { + $destination = $svc_forward->dst; + } + + #done setting variables for the command + + $self->shellcommands_queue( $svc_forward->svcnum, + user => $self->option('user')||'root', + host => $self->machine, + command => eval(qq("$command")), + ); +} + +sub _export_replace { + my( $self, $new, $old ) = (shift, shift, shift); + my $command = $self->option('usermod'); + + #set variable for the command + no strict 'vars'; + { + no strict 'refs'; + ${"old_$_"} = $old->getfield($_) foreach $old->fields; + ${"new_$_"} = $new->getfield($_) foreach $new->fields; + } + + my $old_svc_acct = $old->srcsvc_acct; + $old_username = $old_svc_acct->username; + $old_domain = $old_svc_acct->domain; + if ($old->dstsvc_acct) { + $old_destination = $old->dstsvc_acct->email; + } else { + $old_destination = $old->dst; + } + + my $new_svc_acct = $new->srcsvc_acct; + $new_username = $new_svc_acct->username; + $new_domain = $new_svc_acct->domain; + if ($new->dstsvc) { + $new_destination = $new->dstsvc_acct->email; + } else { + $new_destination = $new->dst; + } + + #done setting variables for the command + + $self->shellcommands_queue( $new->svcnum, + user => $self->option('user')||'root', + host => $self->machine, + command => eval(qq("$command")), + ); +} + +#a good idea to queue anything that could fail or take any time +sub shellcommands_queue { + my( $self, $svcnum ) = (shift, shift); + my $queue = new FS::queue { + 'svcnum' => $svcnum, + 'job' => "FS::part_export::forward_shellcommands::ssh_cmd", + }; + $queue->insert( @_ ); +} + +sub ssh_cmd { #subroutine, not method + use Net::SSH '0.08'; + &Net::SSH::ssh_cmd( { @_ } ); +} + +#sub shellcommands_insert { #subroutine, not method +#} +#sub shellcommands_replace { #subroutine, not method +#} +#sub shellcommands_delete { #subroutine, not method +#} + diff --git a/FS/FS/part_export/infostreet.pm b/FS/FS/part_export/infostreet.pm index f2d519932..caca7c5e1 100644 --- a/FS/FS/part_export/infostreet.pm +++ b/FS/FS/part_export/infostreet.pm @@ -55,6 +55,12 @@ sub _export_insert { $err_or_queue = $self->infostreet_queueContact( $svc_acct->svcnum, $svc_acct->username, %contact_info ); return $err_or_queue unless ref($err_or_queue); + + # If a quota has been specified set the quota because it is not the default + $err_or_queue = $self->infostreet_queueSetQuota( $svc_acct->svcnum, + $svc_acct->username, $svc_acct->quota ) if $svc_acct->quota; + return $err_or_queue unless ref($err_or_queue); + my $error = $err_or_queue->depend_insert( $jobnum ); return $error if $error; @@ -68,6 +74,13 @@ sub _export_replace { my( $self, $new, $old ) = (shift, shift, shift); return "can't change username with InfoStreet" if $old->username ne $new->username; + + # If the quota has changed then do the export to setQuota + my $err_or_queue = $self->infostreet_queueSetQuota( $new->svcnum, $new->username, $new->quota ) + if ( $old->quota != $new->quota ); + return $err_or_queue unless ref($err_or_queue); + + return '' unless $old->_password ne $new->_password; $self->infostreet_queue( $new->svcnum, 'passwd', $new->username, $new->_password ); @@ -150,6 +163,30 @@ sub infostreet_setContact { } +sub infostreet_queueSetQuota { + + my( $self, $svcnum) = (shift, shift); + my $queue = new FS::queue { + 'svcnum' => $svcnum, + 'job' => 'FS::part_export::infostreet::infostreet_setQuota', + }; + + $queue->insert( + $self->option('url'), + $self->option('login'), + $self->option('password'), + $self->option('groupID'), + @_, + ) or $queue; + +} + +sub infostreet_setQuota { + my($url, $is_username, $is_password, $groupID, $username, $quota) = @_; + infostreet_command($url, $is_username, $is_password, $groupID, 'setQuota', $username, [ 'int'=> $quota ] ); +} + + sub infostreet_command { #subroutine, not method my($url, $username, $password, $groupID, $method, @args) = @_; diff --git a/FS/FS/part_export/ldap.pm b/FS/FS/part_export/ldap.pm new file mode 100644 index 000000000..57fd1f3f4 --- /dev/null +++ b/FS/FS/part_export/ldap.pm @@ -0,0 +1,253 @@ +package FS::part_export::ldap; + +use vars qw(@ISA @saltset); +use FS::Record qw( dbh ); +use FS::part_export; + +@ISA = qw(FS::part_export); + +@saltset = ( 'a'..'z' , 'A'..'Z' , '0'..'9' , '.' , '/' ); + +sub rebless { shift; } + +sub _export_insert { + my($self, $svc_acct) = (shift, shift); + + #false laziness w/shellcommands.pm + { + no strict 'refs'; + ${$_} = $svc_acct->getfield($_) foreach $svc_acct->fields; + ${$_} = $svc_acct->$_() foreach qw( domain ); + my $cust_pkg = $svc_acct->cust_svc->cust_pkg; + if ( $cust_pkg ) { + my $cust_main = $cust_pkg->cust_main; + ${$_} = $cust_main->getfield($_) foreach qw(first last); + } + } + $crypt_password = ''; #surpress "used only once" warnings + $crypt_password = '{crypt}'. crypt( $svc_acct->_password, + $saltset[int(rand(64))].$saltset[int(rand(64))] ); + + my $username_attrib; + my %attrib = map { /^\s*(\w+)\s+(.*\S)\s*$/; + $username_attrib = $1 if $2 eq '$username'; + ( $1 => eval(qq("$2")) ); } + grep { /^\s*(\w+)\s+(.*\S)\s*$/ } + split("\n", $self->option('attributes')); + + if ( $self->option('radius') ) { + foreach my $table (qw(reply check)) { + my $method = "radius_$table"; + my %radius = $svc_acct->$method(); + foreach my $radius ( keys %radius ) { + ( my $ldap = $radius ) =~ s/\-//g; + $attrib{$ldap} = $radius{$radius}; + } + } + } + + my $err_or_queue = $self->ldap_queue( $svc_acct->svcnum, 'insert', + #$svc_acct->username, + $username_attrib, + %attrib ); + return $err_or_queue unless ref($err_or_queue); + + #groups with LDAP? + #my @groups = $svc_acct->radius_groups; + #if ( @groups ) { + # my $err_or_queue = $self->ldap_queue( + # $svc_acct->svcnum, 'usergroup_insert', + # $svc_acct->username, @groups ); + # return $err_or_queue unless ref($err_or_queue); + #} + + ''; +} + +sub _export_replace { + my( $self, $new, $old ) = (shift, shift, 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'; + + return "can't (yet?) change username with ldap" + if $old->username ne $new->username; + + return "ldap replace unimplemented"; + + my $oldAutoCommit = $FS::UID::AutoCommit; + local $FS::UID::AutoCommit = 0; + my $dbh = dbh; + + my $jobnum = ''; + #if ( $old->username ne $new->username ) { + # my $err_or_queue = $self->ldap_queue( $new->svcnum, 'rename', + # $new->username, $old->username ); + # unless ( ref($err_or_queue) ) { + # $dbh->rollback if $oldAutoCommit; + # return $err_or_queue; + # } + # $jobnum = $err_or_queue->jobnum; + #} + + foreach my $table (qw(reply check)) { + my $method = "radius_$table"; + my %new = $new->$method(); + my %old = $old->$method(); + if ( grep { !exists $old{$_} #new attributes + || $new{$_} ne $old{$_} #changed + } keys %new + ) { + my $err_or_queue = $self->ldap_queue( $new->svcnum, 'insert', + $table, $new->username, %new ); + unless ( ref($err_or_queue) ) { + $dbh->rollback if $oldAutoCommit; + return $err_or_queue; + } + if ( $jobnum ) { + my $error = $err_or_queue->depend_insert( $jobnum ); + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + } + } + + my @del = grep { !exists $new{$_} } keys %old; + if ( @del ) { + my $err_or_queue = $self->ldap_queue( $new->svcnum, 'attrib_delete', + $table, $new->username, @del ); + unless ( ref($err_or_queue) ) { + $dbh->rollback if $oldAutoCommit; + return $err_or_queue; + } + if ( $jobnum ) { + my $error = $err_or_queue->depend_insert( $jobnum ); + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + } + } + } + + # (sorta) false laziness with FS::svc_acct::replace + my @oldgroups = @{$old->usergroup}; #uuuh + my @newgroups = $new->radius_groups; + my @delgroups = (); + foreach my $oldgroup ( @oldgroups ) { + if ( grep { $oldgroup eq $_ } @newgroups ) { + @newgroups = grep { $oldgroup ne $_ } @newgroups; + next; + } + push @delgroups, $oldgroup; + } + + if ( @delgroups ) { + my $err_or_queue = $self->ldap_queue( $new->svcnum, 'usergroup_delete', + $new->username, @delgroups ); + unless ( ref($err_or_queue) ) { + $dbh->rollback if $oldAutoCommit; + return $err_or_queue; + } + if ( $jobnum ) { + my $error = $err_or_queue->depend_insert( $jobnum ); + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + } + } + + if ( @newgroups ) { + my $err_or_queue = $self->ldap_queue( $new->svcnum, 'usergroup_insert', + $new->username, @newgroups ); + unless ( ref($err_or_queue) ) { + $dbh->rollback if $oldAutoCommit; + return $err_or_queue; + } + if ( $jobnum ) { + my $error = $err_or_queue->depend_insert( $jobnum ); + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + } + } + + $dbh->commit or die $dbh->errstr if $oldAutoCommit; + + ''; +} + +sub _export_delete { + my( $self, $svc_acct ) = (shift, shift); + return "ldap delete unimplemented"; + my $err_or_queue = $self->ldap_queue( $svc_acct->svcnum, 'delete', + $svc_acct->username ); + ref($err_or_queue) ? '' : $err_or_queue; +} + +sub ldap_queue { + my( $self, $svcnum, $method ) = (shift, shift, shift); + my $queue = new FS::queue { + 'svcnum' => $svcnum, + 'job' => "FS::part_export::ldap::ldap_$method", + }; + $queue->insert( + $self->machine, + $self->option('dn'), + $self->option('password'), + $self->option('userdn'), + @_, + ) or $queue; +} + +sub ldap_insert { #subroutine, not method + my $ldap = ldap_connect(shift, shift, shift); + my( $userdn, $username_attrib, %attrib ) = @_; + + $userdn = "$username_attrib=$attrib{$username_attrib}, $userdn" + if $username_attrib; + #icky hack, but should be unsurprising to the LDAPers + foreach my $key ( grep { $attrib{$_} =~ /,/ } keys %attrib ) { + $attrib{$key} = [ split(/,/, $attrib{$key}) ]; + } + + my $status = $ldap->add( $userdn, attrs => [ %attrib ] ); + die 'LDAP error: '. $status->error. "\n" if $status->is_error; + + $ldap->unbind; +} + +#sub ldap_delete { #subroutine, not method +# my $dbh = ldap_connect(shift, shift, shift); +# my $username = shift; +# +# foreach my $table (qw( radcheck radreply usergroup )) { +# my $sth = $dbh->prepare( "DELETE FROM $table WHERE UserName = ?" ); +# $sth->execute($username) +# or die "can't delete from $table table: ". $sth->errstr; +# } +# $dbh->disconnect; +#} + +sub ldap_connect { + my( $machine, $dn, $password ) = @_; + my %bind_options; + $bind_options{password} = $password if length($password); + + eval "use Net::LDAP"; + die $@ if $@; + + my $ldap = Net::LDAP->new($machine) or die $@; + my $status = $ldap->bind( $dn, %bind_options ); + die 'LDAP error: '. $status->error. "\n" if $status->is_error; + + $ldap; +} + diff --git a/FS/FS/part_export/postfix.pm b/FS/FS/part_export/postfix.pm new file mode 100644 index 000000000..6d5e449ca --- /dev/null +++ b/FS/FS/part_export/postfix.pm @@ -0,0 +1,7 @@ +package FS::part_export::postfix; + +use vars qw(@ISA); +use FS::part_export::null; + +@ISA = qw(FS::part_export::null); + diff --git a/FS/FS/part_export/router.pm b/FS/FS/part_export/router.pm new file mode 100644 index 000000000..07b5b9edb --- /dev/null +++ b/FS/FS/part_export/router.pm @@ -0,0 +1,166 @@ +package FS::part_export::router; + +=head1 FS::part_export::router + +This export connects to a router and transmits commands via telnet or SSH. +It requires the following custom router fields: + +=over 4 + +=item admin_address - IP address (or hostname) to connect + +=item admin_user - username for admin access + +=item admin_password - password for admin access + +=back + +The export itself needs the following options: + +=over 4 + +=item insert, replace, delete - command strings (to be interpolated) + +=item Prompt - prompt string to expect from router after successful login + +=item Timeout - time to wait for prompt string + +=back + +(Prompt and Timeout are required only for telnet connections.) + +=cut + +use vars qw(@ISA @saltset); +use String::ShellQuote; +use FS::part_export; + +@ISA = qw(FS::part_export); + +@saltset = ( 'a'..'z' , 'A'..'Z' , '0'..'9' , '.' , '/' ); + +sub rebless { shift; } + +sub _export_insert { + my($self) = shift; + $self->_export_command('insert', @_); +} + +sub _export_delete { + my($self) = shift; + $self->_export_command('delete', @_); +} + +sub _export_suspend { + my($self) = shift; + $self->_export_command('suspend', @_); +} + +sub _export_unsuspend { + my($self) = shift; + $self->_export_command('unsuspend', @_); +} + +sub _export_command { + my ( $self, $action, $svc_broadband) = (shift, shift, shift); + my $command = $self->option($action); + return '' if $command =~ /^\s*$/; + + no strict 'vars'; + { + no strict 'refs'; + ${$_} = $svc_broadband->getfield($_) foreach $svc_broadband->fields; + } + # fetch router info + my $router = $svc_broadband->addr_block->router; + my %r; + $r{$_} = $router->getfield($_) foreach $router->virtual_fields; + #warn qq("$command"); + #warn eval(qq("$command")); + + warn "admin_address: '$r{admin_address}'"; + + if ($r{admin_address} ne '') { + $self->router_queue( $svc_broadband->svcnum, $self->option('protocol'), + user => $r{admin_user}, + password => $r{admin_password}, + host => $r{admin_address}, + Timeout => $self->option('Timeout'), + Prompt => $self->option('Prompt'), + command => eval(qq("$command")), + ); + } else { + return ''; + } +} + +sub _export_replace { + + # We don't handle the case of a svc_broadband moving between routers. + # If you want to do that, reprovision the service. + + my($self, $new, $old ) = (shift, shift, shift); + my $command = $self->option('replace'); + no strict 'vars'; + { + no strict 'refs'; + ${"old_$_"} = $old->getfield($_) foreach $old->fields; + ${"new_$_"} = $new->getfield($_) foreach $new->fields; + } + + my $router = $new->addr_block->router; + my %r; + $r{$_} = $router->getfield($_) foreach $router->virtual_fields; + + if ($r{admin_address} ne '') { + $self->router_queue( $new->svcnum, $self->option('protocol'), + user => $r{admin_user}, + password => $r{admin_password}, + host => $r{admin_address}, + Timeout => $self->option('Timeout'), + Prompt => $self->option('Prompt'), + command => eval(qq("$command")), + ); + } else { + return ''; + } +} + +#a good idea to queue anything that could fail or take any time +sub router_queue { + #warn join ':', @_; + my( $self, $svcnum, $protocol ) = (shift, shift, shift); + my $queue = new FS::queue { + 'svcnum' => $svcnum, + }; + $queue->job ("FS::part_export::router::".$protocol."_cmd"); + $queue->insert( @_ ); +} + +sub ssh_cmd { #subroutine, not method + use Net::SSH '0.08'; + &Net::SSH::ssh_cmd( { @_ } ); +} + +sub telnet_cmd { + use Net::Telnet; + + warn join(', ', @_); + + my %arg = @_; + + my $t = new Net::Telnet (Timeout => $arg{Timeout}, + Prompt => $arg{Prompt}); + $t->open($arg{host}); + $t->login($arg{user}, $arg{password}); + my @error = $t->cmd($arg{command}); + die @error if (grep /^ERROR/, @error); +} + +#sub router_insert { #subroutine, not method +#} +#sub router_replace { #subroutine, not method +#} +#sub router_delete { #subroutine, not method +#} + diff --git a/FS/FS/part_export/shellcommands.pm b/FS/FS/part_export/shellcommands.pm index e4005761b..db2e7aaf9 100644 --- a/FS/FS/part_export/shellcommands.pm +++ b/FS/FS/part_export/shellcommands.pm @@ -20,18 +20,49 @@ sub _export_delete { $self->_export_command('userdel', @_); } +sub _export_suspend { + my($self) = shift; + $self->_export_command('suspend', @_); +} + +sub _export_unsuspend { + my($self) = shift; + $self->_export_command('unsuspend', @_); +} + sub _export_command { my ( $self, $action, $svc_acct) = (shift, shift, shift); my $command = $self->option($action); + return '' if $command =~ /^\s*$/; my $stdin = $self->option($action."_stdin"); + + no strict 'vars'; { no strict 'refs'; ${$_} = $svc_acct->getfield($_) foreach $svc_acct->fields; + + my $count = 1; + foreach my $acct_snarf ( $svc_acct->acct_snarf ) { + ${"snarf_$_$count"} = shell_quote( $acct_snarf->get($_) ) + foreach qw( machine username _password ); + $count++; + } + } + + my $cust_pkg = $svc_acct->cust_svc->cust_pkg; + if ( $cust_pkg ) { + $email = ( grep { $_ ne 'POST' } $cust_pkg->cust_main->invoicing_list )[0]; + } else { + $email = ''; } + $finger = shell_quote $finger; + $quoted_password = shell_quote $_password; + $domain = $svc_acct->domain; $crypt_password = ''; #surpress "used only once" warnings $crypt_password = crypt( $svc_acct->_password, $saltset[int(rand(64))].$saltset[int(rand(64))] ); + $self->shellcommands_queue( $svc_acct->svcnum, user => $self->option('user')||'root', host => $self->machine, @@ -44,15 +75,37 @@ sub _export_replace { my($self, $new, $old ) = (shift, shift, shift); my $command = $self->option('usermod'); my $stdin = $self->option('usermod_stdin'); + no strict 'vars'; { no strict 'refs'; ${"old_$_"} = $old->getfield($_) foreach $old->fields; ${"new_$_"} = $new->getfield($_) foreach $new->fields; } $new_finger = shell_quote $new_finger; + $quoted_new__password = shell_quote $new__password; #old, wrong? + $new_quoted_password = shell_quote $new__password; #new, better? + $old_domain = $old->domain; + $new_domain = $new->domain; $new_crypt_password = ''; #surpress "used only once" warnings $new_crypt_password = crypt( $new->_password, $saltset[int(rand(64))].$saltset[int(rand(64))]); + if ( $self->option('usermod_pwonly') ) { + my $error = ''; + if ( $old_username ne $new_username ) { + $error ||= "can't change username"; + } + if ( $old_domain ne $new_domain ) { + $error ||= "can't change domain"; + } + if ( $old_uid != $new_uid ) { + $error ||= "can't change uid"; + } + if ( $old_dir ne $new_dir ) { + $error ||= "can't change dir"; + } + return $error. ' ('. $self->exporttype. ' to '. $self->machine. ')' + if $error; + } $self->shellcommands_queue( $new->svcnum, user => $self->option('user')||'root', host => $self->machine, @@ -72,7 +125,7 @@ sub shellcommands_queue { } sub ssh_cmd { #subroutine, not method - use Net::SSH '0.06'; + use Net::SSH '0.08'; &Net::SSH::ssh_cmd( { @_ } ); } diff --git a/FS/FS/part_export/sqlmail.pm b/FS/FS/part_export/sqlmail.pm index 4194daf0c..8ccad3c7e 100644 --- a/FS/FS/part_export/sqlmail.pm +++ b/FS/FS/part_export/sqlmail.pm @@ -1,54 +1,75 @@ package FS::part_export::sqlmail; -use vars qw(@ISA %fs_mail_table %fields); +use vars qw(@ISA); +use Digest::MD5 qw(md5_hex); +use FS::Record qw(qsearchs); use FS::part_export; +use FS::svc_domain; @ISA = qw(FS::part_export); -%fs_mail_table = ( svc_acct => 'user', - svc_domain => 'domain' ); - -# fields that need to be copied into the fs_mail tables -$fields{user} = [qw(username _password finger domsvc svcnum )]; -$fields{domain} = [qw(domain svcnum catchall )]; - sub rebless { shift; } sub _export_insert { my($self, $svc) = (shift, shift); # this is a svc_something. - my $table = $fs_mail_table{$svc->cust_svc->part_svc->svcdb}; - my @attrib = map {$svc->$_} @{$fields{$table}}; + my $svcdb = $svc->cust_svc->part_svc->svcdb; + my $export_table = $self->option($svcdb . '_table') + or die('Export table not defined for svcdb: ' . $svcdb); + my @export_fields = split(/\s+/, $self->option($svcdb . '_fields')); + my $svchash = update_values($self, $svc, $svcdb); + + foreach my $key (keys(%$svchash)) { + unless (grep { $key eq $_ } @export_fields) { + delete $svchash->{$key}; + } + } + my $error = $self->sqlmail_queue( $svc->svcnum, 'insert', - $table, @attrib ); + $self->option('server_type'), $export_table, + (map { ($_, $svchash->{$_}); } keys(%$svchash))); return $error if $error; ''; + } sub _export_replace { my( $self, $new, $old ) = (shift, shift, shift); - my $table = $fs_mail_table{$new->cust_svc->part_svc->svcdb}; - - my @old = ($old->svcnum, 'delete', $table, $old->svcnum); - my @narf = map {$new->$_} @{$fields{$table}}; - $self->sqlmail_queue($new->svcnum, 'replace', $table, - $new->svcnum, @narf); - + my $svcdb = $new->cust_svc->part_svc->svcdb; + my $export_table = $self->option($svcdb . '_table') + or die('Export table not defined for svcdb: ' . $svcdb); + my @export_fields = split(/\s+/, $self->option($svcdb . '_fields')); + my $svchash = update_values($self, $new, $svcdb); + + foreach my $key (keys(%$svchash)) { + unless (grep { $key eq $_ } @export_fields) { + delete $svchash->{$key}; + } + } + + my $error = $self->sqlmail_queue( $new->svcnum, 'replace', + $old->svcnum, $self->option('server_type'), $export_table, + (map { ($_, $svchash->{$_}); } keys(%$svchash))); return $error if $error; ''; + } sub _export_delete { my( $self, $svc ) = (shift, shift); - my $table = $fs_mail_table{$new->cust_svc->part_svc->svcdb}; + + my $svcdb = $svc->cust_svc->part_svc->svcdb; + my $table = $self->option($svcdb . '_table') + or die('Export table not defined for svcdb: ' . $svcdb); + $self->sqlmail_queue( $svc->svcnum, 'delete', $table, $svc->svcnum ); } sub sqlmail_queue { - my( $self, $svcnum, $method, $table ) = (shift, shift, shift); + my( $self, $svcnum, $method ) = (shift, shift, shift); my $queue = new FS::queue { 'svcnum' => $svcnum, 'job' => "FS::part_export::sqlmail::sqlmail_$method", @@ -63,49 +84,99 @@ sub sqlmail_queue { sub sqlmail_insert { #subroutine, not method my $dbh = sqlmail_connect(shift, shift, shift); - my( $table, @attrib ) = @_; + my( $server_type, $table ) = (shift, shift); - my $sth = $dbh->prepare( - "INSERT INTO $table (" . join (',', @{$fields{$table}}) . - ") VALUES ('" . join ("','", @attrib) . "')" - ) or die $dbh->errstr; - $sth->execute() or die $sth->errstr; + my %attrs = @_; + map { $attrs{$_} = $attrs{$_} ? qq!'$attrs{$_}'! : 'NULL'; } keys(%attrs); + my $query = sprintf("INSERT INTO %s (%s) values (%s)", + $table, join(",", keys(%attrs)), + join(',', values(%attrs))); + + $dbh->do($query) or die $dbh->errstr; $dbh->disconnect; + + ''; } sub sqlmail_delete { #subroutine, not method my $dbh = sqlmail_connect(shift, shift, shift); my( $table, $svcnum ) = @_; - my $sth = $dbh->prepare( - "DELETE FROM $table WHERE svcnum = $svcnum" - ) or die $dbh->errstr; - $sth->execute() or die $sth->errstr; - + $dbh->do("DELETE FROM $table WHERE svcnum = $svcnum") or die $dbh->errstr; $dbh->disconnect; + + ''; } sub sqlmail_replace { my $dbh = sqlmail_connect(shift, shift, shift); - my( $table, $svcnum, @attrib ) = @_; - - my %data; - @data{@{$fields{$table}}} = @attrib; - - my $sth = $dbh->prepare( - "UPDATE $table SET " . - ( join ',', map {$_ . "='" . $data{$_} . "'"} keys(%data) ) . - " WHERE svcnum = $svcnum" - ) or die $dbh->errstr; - $sth->execute() or die $sth->errstr; + my($oldsvcnum, $server_type, $table) = (shift, shift, shift); + + my %attrs = @_; + map { $attrs{$_} = $attrs{$_} ? qq!'$attrs{$_}'! : 'NULL'; } keys(%attrs); + + my $query = "SELECT COUNT(*) FROM $table WHERE svcnum = $oldsvcnum"; + my $result = $dbh->selectrow_arrayref($query) or die $dbh->errstr; + + if (@$result[0] == 0) { + $query = sprintf("INSERT INTO %s (%s) values (%s)", + $table, join(",", keys(%attrs)), + join(',', values(%attrs))); + $dbh->do($query) or die $dbh->errstr; + } else { + $query = sprintf('UPDATE %s SET %s WHERE svcnum = %s', + $table, join(', ', map {"$_ = $attrs{$_}"} keys(%attrs)), + $oldsvcnum); + $dbh->do($query) or die $dbh->errstr; + } $dbh->disconnect; + + ''; } sub sqlmail_connect { - #my($datasrc, $username, $password) = @_; - #DBI->connect($datasrc, $username, $password) or die $DBI::errstr; DBI->connect(@_) or die $DBI::errstr; } +sub update_values { + + # Update records to conform to a particular server_type. + + my ($self, $svc, $svcdb) = (shift,shift,shift); + my $svchash = { %{$svc->hashref} } or return ''; # We need a copy. + + if ($svcdb eq 'svc_acct') { + if ($self->option('server_type') eq 'courier_crypt') { + my $salt = join '', ('.', '/', 0..9,'A'..'Z', 'a'..'z')[rand 64, rand 64]; + $svchash->{_password} = crypt($svchash->{_password}, $salt); + + } elsif ($self->option('server_type') eq 'dovecot_plain') { + $svchash->{_password} = '{PLAIN}' . $svchash->{_password}; + + } elsif ($self->option('server_type') eq 'dovecot_crypt') { + my $salt = join '', ('.', '/', 0..9,'A'..'Z', 'a'..'z')[rand 64, rand 64]; + $svchash->{_password} = '{CRYPT}' . crypt($svchash->{_password}, $salt); + + } elsif ($self->option('server_type') eq 'dovecot_digest_md5') { + my $svc_domain = qsearchs('svc_domain', { svcnum => $svc->domsvc }); + die('Unable to lookup svc_domain with domsvc: ' . $svc->domsvc) + unless ($svc_domain); + + my $domain = $svc_domain->domain; + my $md5hash = '{DIGEST-MD5}' . md5_hex(join(':', $svchash->{username}, + $domain, $svchash->{_password})); + $svchash->{_password} = $md5hash; + } + } elsif ($svcdb eq 'svc_forward') { + if ($self->option('resolve_dstsvc') && $svc->dstsvc_acct) { + $svchash->{dst} = $svc->dstsvc_acct->username . '@' . + $svc->dstsvc_acct->svc_domain->domain; + } + } + + return($svchash); + +} + diff --git a/FS/FS/part_export/sqlradius.pm b/FS/FS/part_export/sqlradius.pm index 3c781c043..8a8f9beba 100644 --- a/FS/FS/part_export/sqlradius.pm +++ b/FS/FS/part_export/sqlradius.pm @@ -8,6 +8,11 @@ use FS::part_export; sub rebless { shift; } +sub export_username { + my($self, $svc_acct) = (shift, shift); + $svc_acct->username; +} + sub _export_insert { my($self, $svc_acct) = (shift, shift); @@ -16,14 +21,14 @@ sub _export_insert { my %attrib = $svc_acct->$method(); next unless keys %attrib; my $err_or_queue = $self->sqlradius_queue( $svc_acct->svcnum, 'insert', - $table, $svc_acct->username, %attrib ); + $table, $self->export_username($svc_acct), %attrib ); return $err_or_queue unless ref($err_or_queue); } my @groups = $svc_acct->radius_groups; if ( @groups ) { my $err_or_queue = $self->sqlradius_queue( $svc_acct->svcnum, 'usergroup_insert', - $svc_acct->username, @groups ); + $self->export_username($svc_acct), @groups ); return $err_or_queue unless ref($err_or_queue); } ''; @@ -44,9 +49,9 @@ sub _export_replace { my $dbh = dbh; my $jobnum = ''; - if ( $old->username ne $new->username ) { + if ( $self->export_username($old) ne $self->export_username($new) ) { my $err_or_queue = $self->sqlradius_queue( $new->svcnum, 'rename', - $new->username, $old->username ); + $self->export_username($new), $self->export_username($old) ); unless ( ref($err_or_queue) ) { $dbh->rollback if $oldAutoCommit; return $err_or_queue; @@ -63,7 +68,7 @@ sub _export_replace { } keys %new ) { my $err_or_queue = $self->sqlradius_queue( $new->svcnum, 'insert', - $table, $new->username, %new ); + $table, $self->export_username($new), %new ); unless ( ref($err_or_queue) ) { $dbh->rollback if $oldAutoCommit; return $err_or_queue; @@ -80,7 +85,7 @@ sub _export_replace { my @del = grep { !exists $new{$_} } keys %old; if ( @del ) { my $err_or_queue = $self->sqlradius_queue( $new->svcnum, 'attrib_delete', - $table, $new->username, @del ); + $table, $self->export_username($new), @del ); unless ( ref($err_or_queue) ) { $dbh->rollback if $oldAutoCommit; return $err_or_queue; @@ -109,7 +114,7 @@ sub _export_replace { if ( @delgroups ) { my $err_or_queue = $self->sqlradius_queue( $new->svcnum, 'usergroup_delete', - $new->username, @delgroups ); + $self->export_username($new), @delgroups ); unless ( ref($err_or_queue) ) { $dbh->rollback if $oldAutoCommit; return $err_or_queue; @@ -125,7 +130,7 @@ sub _export_replace { if ( @newgroups ) { my $err_or_queue = $self->sqlradius_queue( $new->svcnum, 'usergroup_insert', - $new->username, @newgroups ); + $self->export_username($new), @newgroups ); unless ( ref($err_or_queue) ) { $dbh->rollback if $oldAutoCommit; return $err_or_queue; @@ -147,7 +152,7 @@ sub _export_replace { sub _export_delete { my( $self, $svc_acct ) = (shift, shift); my $err_or_queue = $self->sqlradius_queue( $svc_acct->svcnum, 'delete', - $svc_acct->username ); + $self->export_username($svc_acct) ); ref($err_or_queue) ? '' : $err_or_queue; } @@ -187,11 +192,15 @@ sub sqlradius_insert { #subroutine, not method } else { my $i_sth = $dbh->prepare( - "INSERT INTO rad$table ( id, UserName, Attribute, Value ) ". + "INSERT INTO rad$table ( UserName, Attribute, op, Value ) ". "VALUES ( ?, ?, ?, ? )" ) or die $dbh->errstr; - $i_sth->execute( '', $username, $attribute, $attributes{$attribute} ) - or die $i_sth->errstr; + $i_sth->execute( + $username, + $attribute, + ( $attribute =~ /Password/i ? '==' : ':=' ), + $attributes{$attribute}, + ) or die $i_sth->errstr; } @@ -204,10 +213,10 @@ sub sqlradius_usergroup_insert { #subroutine, not method my( $username, @groups ) = @_; my $sth = $dbh->prepare( - "INSERT INTO usergroup ( id, UserName, GroupName ) VALUES ( ?, ?, ? )" + "INSERT INTO usergroup ( UserName, GroupName ) VALUES ( ?, ? )" ) or die $dbh->errstr; foreach my $group ( @groups ) { - $sth->execute( '', $username, $group ) + $sth->execute( $username, $group ) or die "can't insert into groupname table: ". $sth->errstr; } $dbh->disconnect; diff --git a/FS/FS/part_export/sqlradius_withdomain.pm b/FS/FS/part_export/sqlradius_withdomain.pm new file mode 100644 index 000000000..1c8f38c9d --- /dev/null +++ b/FS/FS/part_export/sqlradius_withdomain.pm @@ -0,0 +1,12 @@ +package FS::part_export::sqlradius_withdomain; + +use vars qw(@ISA); +use FS::part_export::sqlradius; + +@ISA = qw(FS::part_export::sqlradius); + +sub export_username { + my($self, $svc_acct) = (shift, shift); + $svc_acct->email; +} + diff --git a/FS/FS/part_export/vpopmail.pm b/FS/FS/part_export/vpopmail.pm index 6a486faa1..a505a0f47 100644 --- a/FS/FS/part_export/vpopmail.pm +++ b/FS/FS/part_export/vpopmail.pm @@ -1,6 +1,7 @@ package FS::part_export::vpopmail; -use vars qw(@ISA @saltset $exportdir $rsync $ssh); +use vars qw(@ISA @saltset $exportdir); +use Fcntl qw(:flock); use File::Path; use FS::UID qw( datasrc ); use FS::part_export; @@ -9,9 +10,6 @@ use FS::part_export; @saltset = ( 'a'..'z' , 'A'..'Z' , '0'..'9' , '.' , '/' ); -$rsync = "rsync"; -$ssh = "ssh"; - sub rebless { shift; } sub _export_insert { @@ -20,6 +18,8 @@ sub _export_insert { $svc_acct->username, crypt($svc_acct->_password,$saltset[int(rand(64))].$saltset[int(rand(64))]), $svc_acct->domain, + $svc_acct->quota, + $svc_acct->finger, ); } @@ -47,7 +47,7 @@ sub _export_replace { return '' unless $old->_password ne $new->_password; $self->vpopmail_queue( $new->svcnum, 'replace', - $new->username, $cpassword, $new->domain ); + $new->username, $cpassword, $new->domain, $new->quota, $new->finger ); } sub _export_delete { @@ -59,25 +59,37 @@ sub _export_delete { #a good idea to queue anything that could fail or take any time sub vpopmail_queue { my( $self, $svcnum, $method ) = (shift, shift, shift); + my $exportdir = "/usr/local/etc/freeside/export." . datasrc; + mkdir $exportdir, 0700 or die $! unless -d $exportdir; + $exportdir .= "/vpopmail"; + mkdir $exportdir, 0700 or die $! unless -d $exportdir; + $exportdir .= '/'. $self->machine; + mkdir $exportdir, 0700 or die $! unless -d $exportdir; + mkdir "$exportdir/domains", 0700 or die $! unless -d "$exportdir/domains"; + my $queue = new FS::queue { 'svcnum' => $svcnum, 'job' => "FS::part_export::vpopmail::vpopmail_$method", }; $queue->insert( $exportdir, - $self->option('machine'), + $self->machine, $self->option('dir'), $self->option('uid'), $self->option('gid'), + $self->option('restart'), @_ ); } sub vpopmail_insert { #subroutine, not method - my( $exportdir, $machine, $dir, $uid, $gid ) = splice @_,0,5; - my( $username, $password, $domain ) = @_; - + my( $exportdir, $machine, $dir, $uid, $gid, $restart ) = splice @_,0,6; + my( $username, $password, $domain, $quota, $finger ) = @_; + + mkdir "$exportdir/domains/$domain", 0700 or die $! + unless -d "$exportdir/domains/$domain"; + (open(VPASSWD, ">>$exportdir/domains/$domain/vpasswd") and flock(VPASSWD,LOCK_EX) ) or die "can't open vpasswd file for $username\@$domain: ". @@ -87,28 +99,28 @@ sub vpopmail_insert { #subroutine, not method $password, '1', '0', - $username, + $finger, "$dir/domains/$domain/$username", - 'NOQUOTA', + $quota ? $quota.'S' : 'NOQUOTA', ), "\n"; flock(VPASSWD,LOCK_UN); close(VPASSWD); for my $mkdir ( - map { "$exportdir/domains/$domain/$username$_" } - ( '', qw( /Maildir /Maildir/cur /Maildir/new /Maildir/tmp ) ) + grep { ! -d $_ } map { "$exportdir/domains/$domain/$username$_" } + ( '', qw( /Maildir /Maildir/cur /Maildir/new /Maildir/tmp ) ) ) { mkdir $mkdir, 0700 or die "can't mkdir $mkdir: $!"; } - vpopmail_sync( $exportdir, $machine, $dir, $uid, $gid ); + vpopmail_sync( $exportdir, $machine, $dir, $uid, $gid, $restart ); } sub vpopmail_replace { #subroutine, not method - my( $exportdir, $machine, $dir, $uid, $gid ) = splice @_,0,5; - my( $username, $password, $domain ) = @_; + my( $exportdir, $machine, $dir, $uid, $gid, $restart ) = splice @_,0,6; + my( $username, $password, $domain, $quota, $finger ) = @_; (open(VPASSWD, "$exportdir/domains/$domain/vpasswd") and flock(VPASSWD,LOCK_EX) @@ -118,10 +130,21 @@ sub vpopmail_replace { #subroutine, not method or die "Can't open $exportdir/domains/$domain/vpasswd.tmp: $!"; while (<VPASSWD>) { - my ($mailbox, $pw, @rest) = split(':', $_); - print VPASSWDTMP $_ unless $username eq $mailbox; - print VPASSWDTMP join (':', ($mailbox, $password, @rest)) - if $username eq $mailbox; + my ($mailbox, $pw, $vuid, $vgid, $vfinger, $vdir, $vquota, @rest) = + split(':', $_); + if ( $username ne $mailbox ) { + print VPASSWDTMP $_; + next + } + print VPASSWDTMP join (':', + $mailbox, + $password, + '1', + '0', + $finger, + "$dir/domains/$domain/$username", #$vdir + $quota ? $quota.'S' : 'NOQUOTA', + ), "\n"; } close(VPASSWDTMP); @@ -132,12 +155,12 @@ sub vpopmail_replace { #subroutine, not method flock(VPASSWD,LOCK_UN); close(VPASSWD); - vpopmail_sync( $exportdir, $machine, $dir, $uid, $gid ); + vpopmail_sync( $exportdir, $machine, $dir, $uid, $gid, $restart ); } sub vpopmail_delete { #subroutine, not method - my( $exportdir, $machine, $dir, $uid, $gid ) = splice @_,0,5; + my( $exportdir, $machine, $dir, $uid, $gid, $restart ) = splice @_,0,6; my( $username, $domain ) = @_; (open(VPASSWD, "$exportdir/domains/$domain/vpasswd") @@ -164,16 +187,40 @@ sub vpopmail_delete { #subroutine, not method rmtree "$exportdir/domains/$domain/$username" or die "can't rmtree $exportdir/domains/$domain/$username: $!"; - vpopmail_sync( $exportdir, $machine, $dir, $uid, $gid ); + vpopmail_sync( $exportdir, $machine, $dir, $uid, $gid, $restart ); } sub vpopmail_sync { - my( $exportdir, $machine, $dir, $uid, $gid ) = splice @_,0,5; + my( $exportdir, $machine, $dir, $uid, $gid, $restart ) = splice @_,0,6; chdir $exportdir; - my @args = ( $rsync, "-rlpt", "-e", $ssh, "domains/", - "vpopmail\@$machine:$dir/domains/" ); - system {$args[0]} @args; +# my @args = ( $rsync, "-rlpt", "-e", $ssh, "domains/", +# "vpopmail\@$machine:$dir/domains/" ); +# system {$args[0]} @args; + + eval "use File::Rsync;"; + die $@ if $@; + + my $rsync = File::Rsync->new({ rsh => 'ssh' }); + + $rsync->exec( { + recursive => 1, + perms => 1, + times => 1, + src => "$exportdir/domains/", + dest => "vpopmail\@$machine:$dir/domains/", + } ); # true/false return value from exec is not working, alas + if ( $rsync->err ) { + die "error uploading to vpopmail\@$machine:$dir/domains/ : ". + 'exit status: '. $rsync->status. ', '. + 'STDERR: '. join(" / ", $rsync->err). ', '. + 'STDOUT: '. join(" / ", $rsync->out); + } + + eval "use Net::SSH qw(ssh);"; + die $@ if $@; + + ssh("vpopmail\@$machine", $restart) if $restart; } diff --git a/FS/FS/part_export/www_shellcommands.pm b/FS/FS/part_export/www_shellcommands.pm index 84c162761..3e0087446 100644 --- a/FS/FS/part_export/www_shellcommands.pm +++ b/FS/FS/part_export/www_shellcommands.pm @@ -23,17 +23,13 @@ sub _export_command { my $command = $self->option($action); #set variable for the command + no strict 'vars'; { no strict 'refs'; ${$_} = $svc_www->getfield($_) foreach $svc_www->fields; } my $domain_record = $svc_www->domain_record; # or die ? - my $zone = $domain_record->reczone; # or die ? - unless ( $zone =~ /\.$/ ) { - my $svc_domain = $domain_record->svc_domain; # or die ? - $zone .= '.'. $svc_domain->domain; - } - + my $zone = $domain_record->zone; # or die ? my $svc_acct = $svc_www->svc_acct; # or die ? my $username = $svc_acct->username; my $homedir = $svc_acct->dir; # or die ? @@ -52,6 +48,7 @@ sub _export_replace { my $command = $self->option('usermod'); #set variable for the command + no strict 'vars'; { no strict 'refs'; ${"old_$_"} = $old->getfield($_) foreach $old->fields; @@ -99,7 +96,7 @@ sub shellcommands_queue { } sub ssh_cmd { #subroutine, not method - use Net::SSH '0.06'; + use Net::SSH '0.08'; &Net::SSH::ssh_cmd( { @_ } ); } diff --git a/FS/FS/part_export_option.pm b/FS/FS/part_export_option.pm index a0b19fde1..33b5e5a67 100644 --- a/FS/FS/part_export_option.pm +++ b/FS/FS/part_export_option.pm @@ -115,7 +115,7 @@ sub check { #check options & values? - ''; #no error + $self->SUPER::check; } =back diff --git a/FS/FS/part_pkg.pm b/FS/FS/part_pkg.pm index e914636e4..dcce66b38 100644 --- a/FS/FS/part_pkg.pm +++ b/FS/FS/part_pkg.pm @@ -2,7 +2,7 @@ package FS::part_pkg; use strict; use vars qw( @ISA ); -use FS::Record qw( qsearch dbh ); +use FS::Record qw( qsearch dbh dbdef ); use FS::pkg_svc; use FS::agent_type; use FS::type_pkgs; @@ -180,6 +180,8 @@ insert and replace methods. sub check { my $self = shift; + for (qw(setup recur)) { $self->set($_=>0) if $self->get($_) =~ /^\s*$/; } + my $conf = new FS::Conf; if ( $conf->exists('safe-part_pkg') ) { @@ -218,6 +220,8 @@ sub check { or $r =~ /^my \$min = \$cust_pkg\->seconds_since\(\$cust_pkg\->bill \|\| 0\) \/ 60 \- \s*\d*\.?\d*\s*; \$min = 0 if \$min < 0; \s*\d*\.?\d*\s* \+ \s*\d*\.?\d*\s* \* \$min;\s*$/ + or $r =~ /^my \$last_bill = \$cust_pkg\->last_bill; my \$hours = \$cust_pkg\->seconds_since_sqlradacct\(\$last_bill, \$sdate \) \/ 3600 - \s*\d\.?\d*\s*; \$hours = 0 if \$hours < 0; my \$input = \$cust_pkg\->attribute_since_sqlradacct\(\$last_bill, \$sdate, "AcctInputOctets" \) \/ 1048576; my \$output = \$cust_pkg\->attribute_since_sqlradacct\(\$last_bill, \$sdate, "AcctOutputOctets" \) \/ 1048576; my \$total = \$input \+ \$output \- \s*\d\.?\d*\s*; \$total = 0 if \$total < 0; my \$input = \$input - \s*\d\.?\d*\s*; \$input = 0 if \$input < 0; my \$output = \$output - \s*\d\.?\d*\s*; \$output = 0 if \$output < 0; \s*\d\.?\d*\s* \+ \s*\d\.?\d*\s* \* \$hours \+ \s*\d\.?\d*\s* \* \$input \+ \s*\d\.?\d*\s* \* \$output \+ \s*\d\.?\d*\s* \* \$total *;\s*$/ + or do { #log! return "illegal recur: $r"; @@ -225,11 +229,19 @@ sub check { } + if ( $self->dbdef_table->column('freq')->type =~ /(int)/i ) { + my $error = $self->ut_number('freq'); + return $error if $error; + } else { + $self->freq =~ /^(\d+[dw]?)$/ + or return "Illegal or empty freq: ". $self->freq; + $self->freq($1); + } + $self->ut_numbern('pkgpart') || $self->ut_text('pkg') || $self->ut_text('comment') || $self->ut_anything('setup') - || $self->ut_number('freq') || $self->ut_anything('recur') || $self->ut_alphan('plan') || $self->ut_anything('plandata') @@ -237,6 +249,7 @@ sub check { || $self->ut_enum('recurtax', [ '', 'Y' ] ) || $self->ut_textn('taxclass') || $self->ut_enum('disabled', [ '', 'Y' ] ) + || $self->SUPER::check ; } @@ -254,20 +267,25 @@ sub pkg_svc { =item svcpart [ SVCDB ] -Returns the svcpart of a single service definition (see L<FS::part_svc>) +Returns the svcpart of the primary service definition (see L<FS::part_svc>) associated with this billing item definition (see L<FS::pkg_svc>). Returns -false if there not exactly one service definition with quantity 1, or if -SVCDB is specified and does not match the svcdb of the service definition, +false if there not a primary service definition or exactly one service +definition with quantity 1, or if SVCDB is specified and does not match the +svcdb of the service definition, =cut sub svcpart { my $self = shift; - my $svcdb = shift; - my @pkg_svc = $self->pkg_svc; - return '' if scalar(@pkg_svc) != 1 - || $pkg_svc[0]->quantity != 1 - || ( $svcdb && $pkg_svc[0]->part_svc->svcdb ne $svcdb ); + my $svcdb = scalar(@_) ? shift : ''; + my @svcdb_pkg_svc = + grep { ( $svcdb eq $_->part_svc->svcdb || !$svcdb ) } $self->pkg_svc; + my @pkg_svc = (); + @pkg_svc = grep { $_->primary_svc =~ /^Y/i } @svcdb_pkg_svc + if dbdef->table('pkg_svc')->column('primary_svc'); + @pkg_svc = grep {$_->quantity == 1 } @svcdb_pkg_svc + unless @pkg_svc; + return '' if scalar(@pkg_svc) != 1; $pkg_svc[0]->svcpart; } @@ -280,6 +298,8 @@ following logic instead; If the package has B<0> setup and B<0> recur, the single item B<BILL> is returned, otherwise, the single item B<CARD> is returned. +(CHEK? LEC? Probably shouldn't accept those by default, prone to abuse) + =cut sub payby { @@ -295,10 +315,6 @@ sub payby { =back -=head1 VERSION - -$Id: part_pkg.pm,v 1.16 2002-06-10 01:39:50 khoff Exp $ - =head1 BUGS The delete method is unimplemented. diff --git a/FS/FS/part_pop_local.pm b/FS/FS/part_pop_local.pm index 0b7cdf6c9..f7d5eac9a 100644 --- a/FS/FS/part_pop_local.pm +++ b/FS/FS/part_pop_local.pm @@ -92,6 +92,7 @@ sub check { or $self->ut_text('state') or $self->ut_number('npa') or $self->ut_number('nxx') + or $self->SUPER::check ; } @@ -100,7 +101,7 @@ sub check { =head1 VERSION -$Id: part_pop_local.pm,v 1.1 2001-09-26 09:17:06 ivan Exp $ +$Id: part_pop_local.pm,v 1.2 2003-08-05 00:20:44 khoff Exp $ =head1 BUGS diff --git a/FS/FS/part_referral.pm b/FS/FS/part_referral.pm index 23885dffd..c0858c0ed 100644 --- a/FS/FS/part_referral.pm +++ b/FS/FS/part_referral.pm @@ -38,6 +38,8 @@ The following fields are currently supported: =item referral - Text name of this advertising source +=item disabled - Disabled flag, empty or 'Y' + =back =head1 NOTE @@ -91,9 +93,17 @@ replace methods. sub check { my $self = shift; - $self->ut_numbern('refnum') + my $error = $self->ut_numbern('refnum') || $self->ut_text('referral') ; + return $error if $error; + + if ( $self->dbdef_table->column('disabled') ) { + $error = $self->ut_enum('disabled', [ '', 'Y' ] ); + return $error if $error; + } + + $self->SUPER::check; } =back diff --git a/FS/FS/part_svc.pm b/FS/FS/part_svc.pm index 959a3f887..aacc3ab48 100644 --- a/FS/FS/part_svc.pm +++ b/FS/FS/part_svc.pm @@ -68,7 +68,7 @@ TODOC: =item I<svcdb>__I<field> - Default or fixed value for I<field> in I<svcdb>. -=item I<svcdb>__I<field>_flag - defines I<svcdb>__I<field> action: null, `D' for default, or `F' for fixed +=item I<svcdb>__I<field>_flag - defines I<svcdb>__I<field> action: null, `D' for default, or `F' for fixed. For virtual fields, can also be 'X' for excluded. TODOC: EXTRA_FIELDS_ARRAYREF @@ -113,7 +113,7 @@ sub insert { } ); my $flag = $self->getfield($svcdb.'__'.$field.'_flag'); - if ( uc($flag) =~ /^([DF])$/ ) { + if ( uc($flag) =~ /^([DFX])$/ ) { $part_svc_column->setfield('columnflag', $1); $part_svc_column->setfield('columnvalue', $self->getfield($svcdb.'__'.$field) @@ -201,7 +201,7 @@ sub replace { } ); my $flag = $new->getfield($svcdb.'__'.$field.'_flag'); - if ( uc($flag) =~ /^([DF])$/ ) { + if ( uc($flag) =~ /^([DFX])$/ ) { $part_svc_column->setfield('columnflag', $1); $part_svc_column->setfield('columnvalue', $new->getfield($svcdb.'__'.$field) @@ -254,32 +254,7 @@ sub check { my @fields = eval { fields( $recref->{svcdb} ) }; #might die return "Unknown svcdb!" unless @fields; -##REPLACED BY part_svc_column -# my $svcdb; -# foreach $svcdb ( qw( -# svc_acct svc_acct_sm svc_domain -# ) ) { -# my @rows = map { /^${svcdb}__(.*)$/; $1 } -# grep ! /_flag$/, -# grep /^${svcdb}__/, -# fields('part_svc'); -# foreach my $row (@rows) { -# unless ( $svcdb eq $recref->{svcdb} ) { -# $recref->{$svcdb.'__'.$row}=''; -# $recref->{$svcdb.'__'.$row.'_flag'}=''; -# next; -# } -# $recref->{$svcdb.'__'.$row.'_flag'} =~ /^([DF]?)$/ -# or return "Illegal flag for $svcdb $row"; -# $recref->{$svcdb.'__'.$row.'_flag'} = $1; -# -# my $error = $self->ut_anything($svcdb.'__'.$row); -# return $error if $error; -# -# } -# } - - ''; #no error + $self->SUPER::check; } =item part_svc_column COLUMNNAME @@ -290,12 +265,12 @@ COLUMNNAME, or a new part_svc_column object if none exists. =cut sub part_svc_column { - my $self = shift; - my $columnname = shift; - qsearchs('part_svc_column', { - 'svcpart' => $self->svcpart, - 'columnname' => $columnname, - } + my( $self, $columnname) = @_; + $self->svcpart && + qsearchs('part_svc_column', { + 'svcpart' => $self->svcpart, + 'columnname' => $columnname, + } ) or new FS::part_svc_column { 'svcpart' => $self->svcpart, 'columnname' => $columnname, @@ -311,22 +286,23 @@ sub all_part_svc_column { qsearch('part_svc_column', { 'svcpart' => $self->svcpart } ); } -=item part_export +=item part_export [ EXPORTTYPE ] + +Returns all exports (see L<FS::part_export>) for this service, or, if an +export type is specified, only returns exports of the given type. =cut sub part_export { my $self = shift; - map { qsearchs('part_export', { 'exportnum' => $_->exportnum } ) } + my %search; + $search{'exporttype'} = shift if @_; + map { qsearchs('part_export', { 'exportnum' => $_->exportnum, %search } ) } qsearch('export_svc', { 'svcpart' => $self->svcpart } ); } =back -=head1 VERSION - -$Id: part_svc.pm,v 1.13 2002-04-11 22:05:31 ivan Exp $ - =head1 BUGS Delete is unimplemented. @@ -334,7 +310,7 @@ Delete is unimplemented. The list of svc_* tables is hardcoded. When svc_acct_pop is renamed, this should be fixed. -all_part_svc_column and part_export methods should be documented +all_part_svc_column method should be documented =head1 SEE ALSO diff --git a/FS/FS/part_svc_column.pm b/FS/FS/part_svc_column.pm index 37e841e87..885155be3 100644 --- a/FS/FS/part_svc_column.pm +++ b/FS/FS/part_svc_column.pm @@ -41,7 +41,7 @@ fields are currently supported: =item columnvalue - default or fixed value for the column -=item columnflag - null, D or F +=item columnflag - null, D, F, X (virtual fields) =back @@ -91,18 +91,18 @@ sub check { ; return $error if $error; - $self->columnflag =~ /^([DF])$/ + $self->columnflag =~ /^([DFX])$/ or return "illegal columnflag ". $self->columnflag; $self->columnflag(uc($1)); - ''; #no error + $self->SUPER::check; } =back =head1 VERSION -$Id: part_svc_column.pm,v 1.1 2001-09-07 20:49:15 ivan Exp $ +$Id: part_svc_column.pm,v 1.2 2003-08-05 00:20:44 khoff Exp $ =head1 BUGS diff --git a/FS/FS/part_svc_router.pm b/FS/FS/part_svc_router.pm new file mode 100755 index 000000000..0b23ab580 --- /dev/null +++ b/FS/FS/part_svc_router.pm @@ -0,0 +1,32 @@ +package FS::part_svc_router; + +use strict; +use vars qw( @ISA ); +use FS::Record qw(qsearchs); +use FS::router; +use FS::part_svc; + +@ISA = qw(FS::Record); + +sub table { 'part_svc_router'; } + +sub check { + my $self = shift; + my $error = + $self->ut_foreign_key('svcpart', 'part_svc', 'svcpart') + || $self->ut_foreign_key('routernum', 'router', 'routernum'); + return $error if $error; + ''; #no error +} + +sub router { + my $self = shift; + return qsearchs('router', { routernum => $self->routernum }); +} + +sub part_svc { + my $self = shift; + return qsearchs('part_svc', { svcpart => $self->svcpart }); +} + +1; diff --git a/FS/FS/part_virtual_field.pm b/FS/FS/part_virtual_field.pm new file mode 100755 index 000000000..03c34cca5 --- /dev/null +++ b/FS/FS/part_virtual_field.pm @@ -0,0 +1,303 @@ +package FS::part_virtual_field; + +use strict; +use vars qw( @ISA ); +use FS::Record qw( qsearchs qsearch dbdef ); + +@ISA = qw( FS::Record ); + +=head1 NAME + +FS::part_virtual_field - Object methods for part_virtual_field records + +=head1 SYNOPSIS + + use FS::part_virtual_field; + + $record = new FS::part_virtual_field \%hash; + $record = new FS::part_virtual_field { 'column' => 'value' }; + + $error = $record->insert; + + $error = $new_record->replace($old_record); + + $error = $record->delete; + + $error = $record->check; + +=head1 DESCRIPTION + +An FS::part_virtual_field object represents the definition of a virtual field +(see the BACKGROUND section). FS::part_virtual_field contains the name and +base table of the field, as well as validation rules and UI hints about the +display of the field. The actual data is stored in FS::virtual_field; see +its manpage for details. + +FS::part_virtual_field inherits from FS::Record. The following fields are +currently supported: + +=over 2 + +=item vfieldpart - primary key (assigned automatically) + +=item name - name of the field + +=item dbtable - table for which this virtual field is defined + +=item check_block - Perl code to validate/normalize data + +=item list_source - Perl code to generate a list of values (UI hint) + +=item length - expected length of the value (UI hint) + +=item label - descriptive label for the field (UI hint) + +=item sequence - sort key (UI hint; unimplemented) + +=back + +=head1 BACKGROUND + +"Form is none other than emptiness, + and emptiness is none other than form." +-- Heart Sutra + +The virtual field mechanism allows site admins to make trivial changes to +the Freeside database schema without modifying the code. Specifically, the +user can add custom-defined 'fields' to the set of data tracked by Freeside +about objects such as customers and services. These fields are not associated +with any logic in the core Freeside system, but may be referenced in peripheral +code such as exports, price calculations, or alternate interfaces, or may just +be stored in the database for future reference. + +This system was originally devised for svc_broadband, which (by necessity) +comprises such a wide range of access technologies that no static set of fields +could contain all the information needed by the exports. In an appalling +display of False Laziness, a parallel mechanism was implemented for the +router table, to store properties such as passwords to configure routers. + +The original system treated svc_broadband custom fields (sb_fields) as records +in a completely separate table. Any code that accessed or manipulated these +fields had to be aware that they were I<not> fields in svc_broadband, but +records in sb_field. For example, code that inserted a svc_broadband with +several custom fields had to create an FS::svc_broadband object, call its +insert() method, and then create several FS::sb_field objects and call I<their> +insert() methods. + +This created a problem for exports. The insert method on any FS::svc_Common +object (including svc_broadband) automatically triggers exports after the +record has been inserted. However, at this point, the sb_fields had not yet +been inserted, so the export could not rely on their presence, which was the +original purpose of sb_fields. + +Hence the new system. Virtual fields are appended to the field list of every +record at the FS::Record level, whether the object is created ex nihilo with +new() or fetched with qsearch(). The fields() method now returns a list of +both real and virtual fields. The insert(), replace(), and delete() methods +now update both the base table and the virtual fields, in a single transaction. + +A new method is provided, virtual_fields(), which gives only the virtual +fields. UI code that dynamically generates form widgets to edit virtual field +data should use this to figure out what fields are defined. (See below.) + +Subclasses may override virtual_fields() to restrict the set of virtual +fields available. Some discipline and sanity on the part of the programmer +are required; in particular, this function should probably not depend on any +fields in the record other than the primary key, since the others may change +after the object is instantiated. (Making it depend on I<virtual> fields is +just asking for pain.) One use of this is seen in FS::svc_Common; another +possibility is field-level access control based on FS::UID::getotaker(). + +As a trivial case, a subclass may opt out of supporting virtual fields with +the following code: + +sub virtual_fields { () } + +=head1 METHODS + +=over 4 + +=item new HASHREF + +Create a new record. To add the record to the database, see "insert". + +=cut + +sub table { 'part_virtual_field'; } +sub virtual_fields { () } + +=item insert + +Adds this record to the database. If there is an error, returns the error, +otherwise returns false. + +=item delete + +Deletes this record from the database. If there is an error, returns the +error, otherwise returns false. + +=item replace OLD_RECORD + +Replaces OLD_RECORD with this one in the database. If there is an error, +returns the error, otherwise returns false. + +=item check + +If there is an error, returns the error, otherwise returns false. +Called by the insert and replace methods. + +=back + +=cut + +sub check { + my $self = shift; + + my $error = $self->ut_text('name') || + $self->ut_text('dbtable') || + $self->ut_number('length') + ; + return $error if $error; + + # Make sure it's a real table with a numeric primary key + my ($table, $pkey); + if($table = $FS::Record::dbdef->table($self->dbtable)) { + if($pkey = $table->primary_key) { + if($table->column($pkey)->type =~ /int/i) { + # this is what it should be + } else { + $error = "$table.$pkey is not an integer"; + } + } else { + $error = "$table does not have a single-field primary key"; + } + } else { + $error = "$table does not exist in the schema"; + } + return $error if $error; + + # Possibly some sanity checks for check_block and list_source? + + $self->SUPER::check; +} + +=item list + +Evaluates list_source. + +=cut + +sub list { + my $self = shift; + return () unless $self->list_source; + + my @opts = eval($self->list_source); + if($@) { + warn $@; + return (); + } else { + return @opts; + } +} + +=item widget UI_TYPE MODE [ VALUE ] + +Generates UI code for a widget suitable for editing/viewing the field, based on +list_source and length. + +The only UI_TYPE currently supported is 'HTML', and the only MODE is 'view'. +Others will be added later. + +In HTML, all widgets are assumed to be table rows. View widgets look like +<TR><TD ALIGN="right">Label</TD><TD BGCOLOR="#ffffff">Value</TD></TR> + +(Most of the display style stuff, such as the colors, should probably go into +a separate module specific to the UI. That can wait, though. The API for +this function won't change.) + +VALUE (optional) is the current value of the field. + +=cut + +sub widget { + my $self = shift; + my ($ui_type, $mode, $value) = @_; + my $text; + my $label = $self->label || $self->name; + + if ($ui_type eq 'HTML') { + if ($mode eq 'view') { + $text = q!<TR><TD ALIGN="right">! . $label . + q!</TD><TD BGCOLOR="#ffffff">! . $value . + q!</TD></TR>! . "\n"; + } elsif ($mode eq 'edit') { + $text = q!<TR><TD ALIGN="right">! . $label . + q!</TD><TD>!; + if ($self->list_source) { + $text .= q!<SELECT NAME="! . $self->name . + q!" SIZE=1>! . "\n"; + foreach ($self->list) { + $text .= q!<OPTION VALUE="! . $_ . q!"!; + $text .= ' SELECTED' if ($_ eq $value); + $text .= '>' . $_ . '</OPTION>' . "\n"; + } + } else { + $text .= q!<INPUT NAME="! . $self->name . + q!" VALUE="! . $value . q!"!; + if ($self->length) { + $text .= q! SIZE="! . $self->length . q!"!; + } + $text .= '>'; + } + $text .= q!</TD></TR>! . "\n"; + } else { + return ''; + } + } else { + return ''; + } + return $text; +} + +=head1 VERSION + +$Id: part_virtual_field.pm,v 1.2 2003-08-05 00:20:45 khoff Exp $ + +=head1 NOTES + +=head2 Semantics of check_block: + +This has been changed from the sb_field implementation to make check_blocks +simpler and more natural to Perl programmers who work on things other than +Freeside. + +The check_block is eval'd with the (proposed) new value of the field in $_, +and the object to be updated in $self. Its return value is ignored. The +check_block may change the value of $_ to override the proposed value, or +call die() (with an appropriate error message) to reject the update entirely; +the error string will be returned as the output of the check() method. + +This makes check_blocks like + +C<s/foo/bar/> + +do what you expect. + +The check_block is expected NOT to do anything freaky to $self, like modifying +other fields or calling $self->check(). You have been warned. + +(FIXME: Rewrite some of the warnings from part_sb_field and insert here.) + +=head1 BUGS + +None. It's absolutely falwless. + +=head1 SEE ALSO + +L<FS::Record>, L<FS::virtual_field> + +=cut + +1; + + diff --git a/FS/FS/pkg_svc.pm b/FS/FS/pkg_svc.pm index 3c544ffd8..ea52176cb 100644 --- a/FS/FS/pkg_svc.pm +++ b/FS/FS/pkg_svc.pm @@ -46,6 +46,8 @@ FS::Record. The following fields are currently supported: =item quantity - Quantity of this service definition that this billing item definition includes +=item primary_svc - primary flag, empty or 'Y' + =back =head1 METHODS @@ -108,7 +110,12 @@ sub check { return "Unknown pkgpart!" unless $self->part_pkg; return "Unknown svcpart!" unless $self->part_svc; - ''; #no error + if ( $self->dbdef_table->column('primary_svc') ) { + $error = $self->ut_enum('primary_svc', [ '', 'Y' ] ); + return $error if $error; + } + + $self->SUPER::check; } =item part_pkg @@ -135,10 +142,6 @@ sub part_svc { =back -=head1 VERSION - -$Id: pkg_svc.pm,v 1.3 2002-06-10 01:39:50 khoff Exp $ - =head1 BUGS =head1 SEE ALSO diff --git a/FS/FS/port.pm b/FS/FS/port.pm index 13455ca89..620030afc 100644 --- a/FS/FS/port.pm +++ b/FS/FS/port.pm @@ -113,7 +113,7 @@ sub check { unless $self->ip || $self->nasport; return "Unknown nasnum" unless qsearchs('nas', { 'nasnum' => $self->nasnum } ); - ''; #no error + $self->SUPER::check; } =item session @@ -133,7 +133,7 @@ sub session { =head1 VERSION -$Id: port.pm,v 1.5 2001-02-14 04:33:06 ivan Exp $ +$Id: port.pm,v 1.6 2003-08-05 00:20:45 khoff Exp $ =head1 BUGS diff --git a/FS/FS/prepay_credit.pm b/FS/FS/prepay_credit.pm index 7ed9b8344..a9d26d151 100644 --- a/FS/FS/prepay_credit.pm +++ b/FS/FS/prepay_credit.pm @@ -108,6 +108,7 @@ sub check { || $self->ut_alpha('identifier') || $self->ut_money('amount') || $self->utnumbern('seconds') + || $self->SUPER::check ; } diff --git a/FS/FS/queue.pm b/FS/FS/queue.pm index d35dc883f..634f7f4bd 100644 --- a/FS/FS/queue.pm +++ b/FS/FS/queue.pm @@ -207,7 +207,7 @@ sub check { $self->status('new') unless $self->status; $self->_date(time) unless $self->_date; - ''; #no error + $self->SUPER::check; } =item args @@ -385,7 +385,7 @@ END =head1 VERSION -$Id: queue.pm,v 1.15 2002-07-02 06:48:59 ivan Exp $ +$Id: queue.pm,v 1.16 2003-08-05 00:20:46 khoff Exp $ =head1 BUGS diff --git a/FS/FS/queue_arg.pm b/FS/FS/queue_arg.pm index 08fe47341..d23ee2afd 100644 --- a/FS/FS/queue_arg.pm +++ b/FS/FS/queue_arg.pm @@ -100,14 +100,14 @@ sub check { ; return $error if $error; - ''; #no error + $self->SUPER::check; } =back =head1 VERSION -$Id: queue_arg.pm,v 1.1 2001-09-11 00:08:18 ivan Exp $ +$Id: queue_arg.pm,v 1.2 2003-08-05 00:20:46 khoff Exp $ =head1 BUGS diff --git a/FS/FS/queue_depend.pm b/FS/FS/queue_depend.pm index 4a4e3c55c..bc910d8e9 100644 --- a/FS/FS/queue_depend.pm +++ b/FS/FS/queue_depend.pm @@ -103,6 +103,7 @@ sub check { $self->ut_numbern('dependnum') || $self->ut_foreign_key('jobnum', 'queue', 'jobnum') || $self->ut_foreign_key('depend_jobnum', 'queue', 'jobnum') + || $self->SUPER::check ; } diff --git a/FS/FS/raddb.pm b/FS/FS/raddb.pm index 497d98450..efeb739bb 100644 --- a/FS/FS/raddb.pm +++ b/FS/FS/raddb.pm @@ -2,1090 +2,1598 @@ package FS::raddb; use vars qw(%attrib); %attrib = ( - 'ascend_bi_directional_au' => 'Ascend-Bi-Directional-Auth', - 'h323_connect_time' => 'h323-connect-time', - 'connect_rate' => 'Connect-Rate', - 'bind_auth_service_grp' => 'Bind_Auth_Service_Grp', - 'usr_callback_type' => 'USR-Callback-Type', - 'erx_primary_wins' => 'ERX-Primary-Wins', - 'ascend_x25_x121_address' => 'Ascend-X25-X121-Address', - 'usr_log_filter_packets' => 'USR-Log-Filter-Packets', - 'annex_addr_resolution_pr' => 'Annex-Addr-Resolution-Protocol', - 'usr_ip_rip_simple_auth_p' => 'USR-IP-RIP-Simple-Auth-Password', - 'dialback_name' => 'Dialback-Name', - 'x_ascend_fr_dce_n392' => 'X-Ascend-FR-DCE-N392', - 'usr_host_type' => 'USR-Host-Type', - 'le_modem_info' => 'LE-Modem-Info', - 'x_ascend_menu_selector' => 'X-Ascend-Menu-Selector', - 'x_ascend_fr_dce_n393' => 'X-Ascend-FR-DCE-N393', - 'ascend_ip_direct' => 'Ascend-IP-Direct', - 'x_ascend_pre_output_octe' => 'X-Ascend-Pre-Output-Octets', - 'x_ascend_ft1_caller' => 'X-Ascend-FT1-Caller', - 'usr_last_callers_number_' => 'USR-Last-Callers-Number-ANI', - 'usr_rmmie_product_code' => 'USR-RMMIE-Product-Code', - 'usr_igmp_robustness' => 'USR-IGMP-Robustness', - 'ms_chap2_success' => 'MS-CHAP2-Success', - 'ascend_home_agent_passwo' => 'Ascend-Home-Agent-Password', - 'acc_bridging_support' => 'Acc-Bridging-Support', - 'annex_transmit_speed' => 'Annex-Transmit-Speed', - 'old_password' => 'Old-Password', - 'x_ascend_metric' => 'X-Ascend-Metric', - 'acc_clearing_location' => 'Acc-Clearing-Location', - 'ascend_multilink_id' => 'Ascend-Multilink-ID', - 'ascend_egress_enabled' => 'Ascend-Egress-Enabled', - 'usr_bridging' => 'USR-Bridging', - 'ascend_assign_ip_server' => 'Ascend-Assign-IP-Server', - 'acc_dns_server_sec' => 'Acc-Dns-Server-Sec', - 'ascend_home_agent_ip_add' => 'Ascend-Home-Agent-IP-Addr', - 'usr_dnis_reauthenticatio' => 'USR-DNIS-ReAuthentication', - 'acc_modem_error_protocol' => 'Acc-Modem-Error-Protocol', - 'ascend_backup' => 'Ascend-Backup', - 'usr_connect_time' => 'USR-Connect-Time', - 'ascend_cbcp_mode' => 'Ascend-CBCP-Mode', - 'usr_rmmie_x2_status' => 'USR-RMMIE-x2-Status', - 'ascend_multicast_gleave_' => 'Ascend-Multicast-GLeave-Delay', - 'erx_ingress_statistics' => 'ERX-Ingress-Statistics', - 'cisco_nas_port' => 'Cisco-NAS-Port', - 'le_admin_group' => 'LE-Admin-Group', - 'annex_mrru' => 'Annex-MRRU', - 'x_ascend_add_seconds' => 'X-Ascend-Add-Seconds', - 'ascend_token_expiry' => 'Ascend-Token-Expiry', - 'usr_igmp_maximum_respons' => 'USR-IGMP-Maximum-Response-Time', - 'ascend_calling_id_presen' => 'Ascend-Calling-Id-Presentatn', - 'connect_info' => 'Connect-Info', - 'ascend_access_intercept_' => 'Ascend-Access-Intercept-LEA', - 'x_ascend_dba_monitor' => 'X-Ascend-DBA-Monitor', - 'client_dns_pri' => 'Client_DNS_Pri', - 'ip_host_addr' => 'Ip_Host_Addr', - 'callback_id' => 'Callback-Id', - 'acct_mcast_out_octets' => 'Acct_Mcast_Out_Octets', - 'acct_input_octets_64' => 'Acct_Input_Octets_64', - 'tunnel_function' => 'Tunnel_Function', - 'ascend_fr_direct_profile' => 'Ascend-FR-Direct-Profile', - 'h323_incoming_conf_id' => 'h323-incoming-conf-id', - 'ascend_ppp_vj_1172' => 'Ascend-PPP-VJ-1172', - 'ms_new_arap_password' => 'MS-New-ARAP-Password', - 'h323_voice_quality' => 'h323-voice-quality', - 'framed_appletalk_network' => 'Framed-AppleTalk-Network', - 'bind_int_interface_name' => 'Bind_Int_Interface_Name', - 'event_timestamp' => 'Event-Timestamp', - 'ascend_bir_enable' => 'Ascend-BIR-Enable', - 'usr_fallback_enabled' => 'USR-Fallback-Enabled', - 'ascend_dhcp_pool_number' => 'Ascend-DHCP-Pool-Number', - 'acct_session_id' => 'Acct-Session-Id', - 'ascend_private_route_req' => 'Ascend-Private-Route-Required', - 'usr_rmmie_pwrlvl_farecho' => 'USR-RMMIE-PwrLvl-FarEcho-Canc', - 'usr_at_input_filter' => 'USR-AT-Input-Filter', - 'erx_egress_statistics' => 'ERX-Egress-Statistics', - 'x_ascend_call_type' => 'X-Ascend-Call-Type', - 'acct_tunnel_client_endpo' => 'Acct-Tunnel-Client-Endpoint', - 'x_ascend_assign_ip_clien' => 'X-Ascend-Assign-IP-Client', - 'ascend_if_netmask' => 'Ascend-IF-Netmask', - 'ascend_dhcp_maximum_leas' => 'Ascend-DHCP-Maximum-Leases', - 'usr_at_output_filter' => 'USR-AT-Output-Filter', - 'usr_rad_dvmrp_metric' => 'USR-Rad-Dvmrp-Metric', - 'rate_limit_rate' => 'Rate_Limit_Rate', - 'prefix' => 'Prefix', - 'ascend_x25_pad_banner' => 'Ascend-X25-Pad-Banner', - 'usr_rmmie_rcv_pwrlvl_375' => 'USR-RMMIE-Rcv-PwrLvl-3750Hz', - 'x_ascend_user_acct_key' => 'X-Ascend-User-Acct-Key', - 'group_name' => 'Group-Name', - 'ascend_receive_secret' => 'Ascend-Receive-Secret', - 'reply_message' => 'Reply-Message', - 'le_nat_sess_dir_fail_act' => 'LE-NAT-Sess-Dir-Fail-Action', - 'framed_callback_id' => 'Framed-Callback-Id', - 'cisco_disconnect_cause' => 'Cisco-Disconnect-Cause', - 'stripped_user_name' => 'Stripped-User-Name', - 'annex_keypress_timeout' => 'Annex-Keypress-Timeout', - 'annex_receive_speed' => 'Annex-Receive-Speed', - 'ms_chap_domain' => 'MS-CHAP-Domain', - 'ascend_atm_connect_group' => 'Ascend-ATM-Connect-Group', - 'usr_send_name' => 'USR-Send-Name', - 'usr_local_framed_ip_addr' => 'USR-Local-Framed-IP-Addr', - 'erx_alternate_cli_vroute' => 'ERX-Alternate-Cli-Vrouter-Name', - 'usr_fallback_limit' => 'USR-Fallback-Limit', - 'ascend_pri_number_type' => 'Ascend-PRI-Number-Type', - 'x_ascend_minimum_channel' => 'X-Ascend-Minimum-Channels', - 'x_ascend_fr_direct_dlci' => 'X-Ascend-FR-Direct-DLCI', - 'ascend_fr_link_mgt' => 'Ascend-FR-Link-Mgt', - 'annex_host_allow' => 'Annex-Host-Allow', - 'x_ascend_force_56' => 'X-Ascend-Force-56', - 'police_burst' => 'Police_Burst', - 'pvc_profile_name' => 'PVC_Profile_Name', + 'usr_at_zip_output_filter' => 'USR-AT-Zip-Output-Filter', 'ms_filter' => 'MS-Filter', - 'rate_limit_burst' => 'Rate_Limit_Burst', - 'ascend_number_sessions' => 'Ascend-Number-Sessions', - 'cisco_call_filter' => 'Cisco-Call-Filter', - 'erx_igmp_enable' => 'ERX-Igmp-Enable', - 'ascend_filter_required' => 'Ascend-Filter-Required', - 'erx_cli_allow_all_vr_acc' => 'ERX-Cli-Allow-All-VR-Access', - 'acc_callback_delay' => 'Acc-Callback-Delay', - 'usr_default_dte_data_rat' => 'USR-Default-DTE-Data-Rate', + 'annex_compression_protoc' => 'Annex-Compression-Protocol', + 'xedia_ssh_privileges' => 'Xedia-SSH-Privileges', + 'usr_blocks_received' => 'USR-Blocks-Received', + 'shiva_called_number' => 'Shiva-Called-Number', + 'annex_filter' => 'Annex-Filter', + 'usr_channel_expansion' => 'USR-Channel-Expansion', + 'erx_tunnel_tos' => 'ERX-Tunnel-Tos', + 'session_timeout' => 'Session-Timeout', + 'ascend_route_ipx' => 'Ascend-Route-IPX', + 'annex_error_correction_p' => 'Annex-Error-Correction-Prot', + 'acc_callback_mode' => 'Acc-Callback-Mode', + 'usr_filter_zones' => 'USR-Filter-Zones', + 'erx_input_gigapkts' => 'ERX-Input-Gigapkts', + 'ascend_session_svr_key' => 'Ascend-Session-Svr-Key', + 'bind_l2tp_tunnel_namf' => 'Bind_L2TP_Tunnel_Name', + 'ascend_dsl_cir_recv_limi' => 'Ascend-Dsl-CIR-Recv-Limit', + 'altiga_secondary_wins_g' => 'Altiga-Secondary-WINS-G', + 'ascend_ts_idle_limit' => 'Ascend-TS-Idle-Limit', + 'usr_port_tap_priority' => 'USR-Port-Tap-Priority', + 'cvpn3000_ipsec_client_fw' => 'CVPN3000-IPSec-Client-Fw-Filter-Name', + 'ascend_private_route_req' => 'Ascend-Private-Route-Required', + 'ascend_private_route' => 'Ascend-Private-Route', + 'prompt' => 'Prompt', + 'acct_link_count' => 'Acct-Link-Count', + 'bind_auth_service_grq' => 'Bind_Auth_Service_Grp', + 'itk_tunnel_ip' => 'ITK-Tunnel-IP', + 'login_lat_node' => 'Login-LAT-Node', + 'usr_mbi_ct_pri_card_slot' => 'USR-Mbi_Ct_PRI_Card_Slot', + 'lac_real_poru' => 'LAC_Real_Port', + 'erx_ingress_statistics' => 'ERX-Ingress-Statistics', + 'digest_nonce' => 'Digest-Nonce', + 'annex_system_disc_reason' => 'Annex-System-Disc-Reason', + 'pool_name' => 'Pool-Name', + 'altiga_use_client_addres' => 'Altiga-Use-Client-Address-G/U', + 'police_bursu' => 'Police_Burst', + 'usr_call_arrival_time' => 'USR-Call-Arrival-Time', + 'ascend_disconnect_cause' => 'Ascend-Disconnect-Cause', + 'ascend_user_acct_time' => 'Ascend-User-Acct-Time', + 'chap_challenge' => 'CHAP-Challenge', + 'ascend_mpp_idle_percent' => 'Ascend-MPP-Idle-Percent', + 'ascend_user_acct_port' => 'Ascend-User-Acct-Port', + 'ldap_group' => 'Ldap-Group', + 'ascend_numbering_plan_id' => 'Ascend-Numbering-Plan-ID', + 'usr_last_number_dialed_o' => 'USR-Last-Number-Dialed-Out', + 'pvc_encapsulation_type' => 'PVC-Encapsulation-Type', + 'ascend_bir_bridge_group' => 'Ascend-BIR-Bridge-Group', + 'ascend_atm_group' => 'Ascend-ATM-Group', + 'ascend_fr_svc_addr' => 'Ascend-FR-SVC-Addr', + 'x_ascend_send_auth' => 'X-Ascend-Send-Auth', 'le_ip_pool' => 'LE-IP-Pool', - 'cisco_pre_output_packets' => 'Cisco-Pre-Output-Packets', - 'x_ascend_group' => 'X-Ascend-Group', - 'usr_channel_connected_to' => 'USR-Channel-Connected-To', - 'usr_ipx_rip_output_filte' => 'USR-IPX-RIP-Output-Filter', - 'usr_esn' => 'USR-ESN', - 'annex_user_level' => 'Annex-User-Level', - 'x_ascend_primary_home_ag' => 'X-Ascend-Primary-Home-Agent', - 'no_such_attribute' => 'No-Such-Attribute', - 'x_ascend_pri_number_type' => 'X-Ascend-PRI-Number-Type', - 'ms_mppe_send_key' => 'MS-MPPE-Send-Key', - 'usr_actual_voltage' => 'USR-Actual-Voltage', - 'annex_acct_servers' => 'Annex-Acct-Servers', - 'ascend_handle_ipx' => 'Ascend-Handle-IPX', - 'cisco_xmit_rate' => 'Cisco-Xmit-Rate', - 'acc_service_profile' => 'Acc-Service-Profile', - 'x_ascend_ara_pw' => 'X-Ascend-Ara-PW', - 'ascend_ckt_type' => 'Ascend-Ckt-Type', - 'cisco_data_rate' => 'Cisco-Data-Rate', - 'group' => 'Group', - 'nas_port' => 'NAS-Port', - 'usr_ipx_call_output_filt' => 'USR-IPX-Call-Output-Filter', - 'tunnel_type' => 'Tunnel-Type', - 'usr_rmmie_manufacturer_i' => 'USR-RMMIE-Manufacturer-ID', - 'user_name_is_star' => 'User-Name-Is-Star', - 'usr_call_arrival_in_gmt' => 'USR-Call-Arrival-in-GMT', - 'x_ascend_number_sessions' => 'X-Ascend-Number-Sessions', - 'ascend_send_auth' => 'Ascend-Send-Auth', - 'user_service_type' => 'User-Service-Type', - 'annex_cli_filter' => 'Annex-CLI-Filter', - 'erx_cli_initial_access_l' => 'ERX-Cli-Initial-Access-Level', - 'ascend_call_direction' => 'Ascend-Call-Direction', - 'usr_chassis_temp_thresho' => 'USR-Chassis-Temp-Threshold', - 'usr_pw_usr_ofilter_ipx' => 'USR-PW_USR_OFilter_IPX', - 'tunnel_session_auth' => 'Tunnel_Session_Auth', - 'x_ascend_connect_progres' => 'X-Ascend-Connect-Progress', - 'ascend_atm_connect_vci' => 'Ascend-ATM-Connect-Vci', - 'x_ascend_maximum_call_du' => 'X-Ascend-Maximum-Call-Duration', - 'usr_rmmie_planned_discon' => 'USR-RMMIE-Planned-Disconnect', - 'x_ascend_fr_dte_n392' => 'X-Ascend-FR-DTE-N392', - 'login_host' => 'Login-Host', - 'ascend_user_acct_host' => 'Ascend-User-Acct-Host', - 'x_ascend_fr_dte_n393' => 'X-Ascend-FR-DTE-N393', - 'acc_tunnel_secret' => 'Acc-Tunnel-Secret', - 'usr_at_rtmp_input_filter' => 'USR-AT-RTMP-Input-Filter', + 'post_proxy_type' => 'Post-Proxy-Type', + 'wispr_session_terminate_' => 'WISPr-Session-Terminate-Time', + 'bintec_pppextiftable' => 'BinTec-pppExtIfTable', + 'nomadix_subnet' => 'Nomadix-Subnet', + 'login_port' => 'Login-Port', + 'ms_chap2_response' => 'MS-CHAP2-Response', + 'ascend_ipsec_profile' => 'Ascend-IPSEC-Profile', + 'usr_compression_algorith' => 'USR-Compression-Algorithm', + 'usr_accm_type' => 'USR-ACCM-Type', + 'simultaneous_use' => 'Simultaneous-Use', + 'cisco_account_info' => 'Cisco-Account-Info', 'framed_protocol' => 'Framed-Protocol', - 'login_callback_number' => 'Login-Callback-Number', - 'ascend_dsl_rate_type' => 'Ascend-Dsl-Rate-Type', + 'erx_tunnel_maximum_sessi' => 'ERX-Tunnel-Maximum-Sessions', + 'redcreek_tunneled_wins_t' => 'RedCreek-Tunneled-WINS-Server2', + 'ascend_recv_name' => 'Ascend-Recv-Name', + 'usr_call_connecting_time' => 'USR-Call-Connecting-Time', + 'quintum_h323_gw_id' => 'Quintum-h323-gw-id', + 'acct_dyn_ac_ent' => 'Acct-Dyn-Ac-Ent', + 'tunnel_remote_name' => 'Tunnel-Remote-Name', + 'annex_ppp_trace_level' => 'Annex-PPP-Trace-Level', + 'cisco_call_type' => 'Cisco-Call-Type', + 'cisco_fax_recipient_coun' => 'Cisco-Fax-Recipient-Count', + 'altiga_ipsec_authenticat' => 'Altiga-IPSec-Authentication-G', + 'wispr_location_id' => 'WISPr-Location-ID', + 'itk_start_delay' => 'ITK-Start-Delay', 'ascend_pre_output_packet' => 'Ascend-Pre-Output-Packets', - 'proxy_state' => 'Proxy-State', - 'usr_pw_usr_ofilter_ip' => 'USR-PW_USR_OFilter_IP', - 'cisco_data_filter' => 'Cisco-Data-Filter', - 'cisco_target_util' => 'Cisco-Target-Util', - 'usr_ids0_call_type' => 'USR-IDS0-Call-Type', - 'usr_blocks_resent' => 'USR-Blocks-Resent', - 'usr_terminal_type' => 'USR-Terminal-Type', - 'ascend_history_weigh_typ' => 'Ascend-History-Weigh-Type', - 'framed_routing' => 'Framed-Routing', - 'ascend_client_assign_dns' => 'Ascend-Client-Assign-DNS', - 'ascend_atm_group' => 'Ascend-ATM-Group', - 'bind_bypass_bypass' => 'Bind_Bypass_Bypass', - 'le_ip_gateway' => 'LE-IP-Gateway', - 'cisco_ip_pool_definition' => 'Cisco-IP-Pool-Definition', - 'x_ascend_maximum_time' => 'X-Ascend-Maximum-Time', - 'usr_request_type' => 'USR-Request-Type', - 'usr_call_arrival_time' => 'USR-Call-Arrival-Time', - 'tunnel_domain' => 'Tunnel_Domain', - 'ms_chap_nt_enc_pw' => 'MS-CHAP-NT-Enc-PW', - 'shiva_calling_number' => 'Shiva-Calling-Number', - 'ip_address_pool_name' => 'Ip_Address_Pool_Name', + 'usr_rmmie_firmware_versi' => 'USR-RMMIE-Firmware-Version', + 'usr_vts_session_key' => 'USR-VTS-Session-Key', + 'ascend_fr_dce_n393' => 'Ascend-FR-DCE-N393', + 'login_host' => 'Login-Host', + 'usr_reply_script3' => 'USR-Reply-Script3', + 'cvpn3000_ipsec_split_tuo' => 'CVPN3000-IPSec-Split-Tunneling-Policy', + 'ascend_pppoe_enable' => 'Ascend-PPPoE-Enable', + 'annex_primary_dns_server' => 'Annex-Primary-DNS-Server', + 'x_ascend_bridge_address' => 'X-Ascend-Bridge-Address', + 'usr_number_of_link_naks' => 'USR-Number-of-Link-NAKs', + 'altiga_priority_on_sep_g' => 'Altiga-Priority-on-SEP-G/U', + 'annex_cli_command' => 'Annex-CLI-Command', + 'usr_pw_framed_routing_v2' => 'USR-PW_Framed_Routing_V2', + 'session_error_codf' => 'Session_Error_Code', + 'annex_user_server_locati' => 'Annex-User-Server-Location', + 'cisco_fax_mdn_address' => 'Cisco-Fax-Mdn-Address', + 'ascend_calling_subaddres' => 'Ascend-Calling-Subaddress', + 'ascend_call_by_call' => 'Ascend-Call-By-Call', + 'ascend_first_dest' => 'Ascend-First-Dest', + 'annex_tunnel_authen_type' => 'Annex-Tunnel-Authen-Type', + 'acct_type' => 'Acct-Type', + 'sql_user_name' => 'SQL-User-Name', 'erx_secondary_dns' => 'ERX-Secondary-Dns', - 'x_ascend_pre_input_octet' => 'X-Ascend-Pre-Input-Octets', - 'ascend_home_agent_udp_po' => 'Ascend-Home-Agent-UDP-Port', - 'le_nat_outsource_inmap' => 'LE-NAT-Outsource-Inmap', - 'x_ascend_home_agent_pass' => 'X-Ascend-Home-Agent-Password', - 'tunnel_password' => 'Tunnel-Password', - 'usr_compression_type' => 'USR-Compression-Type', - 'usr_connect_speed' => 'USR-Connect-Speed', - 'usr_connect_time_limit' => 'USR-Connect-Time-Limit', - 'arap_challenge_response' => 'ARAP-Challenge-Response', - 'ms_link_utilization_thre' => 'MS-Link-Utilization-Threshold', - 'usr_mp_edo' => 'USR-MP-EDO', - 'usr_primary_nbns_server' => 'USR-Primary_NBNS_Server', - 'usr_imsi' => 'USR-IMSI', - 'ascend_fr_direct' => 'Ascend-FR-Direct', - 'ascend_vrouter_name' => 'Ascend-VRouter-Name', - 'ascend_preempt_limit' => 'Ascend-Preempt-Limit', - 'ascend_ip_pool_definitio' => 'Ascend-IP-Pool-Definition', - 'h323_gw_id' => 'h323-gw-id', - 'usr_framed_ipx_route' => 'USR-Framed-IPX-Route', - 'x_ascend_maximum_channel' => 'X-Ascend-Maximum-Channels', - 'login_lat_node' => 'Login-LAT-Node', - 'acct_session_time' => 'Acct-Session-Time', - 'ascend_disconnect_cause' => 'Ascend-Disconnect-Cause', - 'ms_mppe_encryption_polic' => 'MS-MPPE-Encryption-Policy', - 'ms_ras_version' => 'MS-RAS-Version', - 'class' => 'Class', - 'caller_id' => 'Caller-ID', - 'ascend_access_intercept_' => 'Ascend-Access-Intercept-Log', - 'ascend_service_type' => 'Ascend-Service-Type', - 'ascend_h323_dialed_time' => 'Ascend-H323-Dialed-Time', - 'exec_program_wait' => 'Exec-Program-Wait', - 'ascend_x25_nui_password_' => 'Ascend-X25-Nui-Password-Prompt', - 'ascend_appletalk_peer_mo' => 'Ascend-Appletalk-Peer-Mode', - 'login_lat_group' => 'Login-LAT-Group', - 'strip_user_name' => 'Strip-User-Name', - 'nas_ip_address' => 'NAS-IP-Address', - 'ascend_maximum_time' => 'Ascend-Maximum-Time', - 'erx_atm_pcr' => 'ERX-Atm-PCR', - 'ascend_client_primary_dn' => 'Ascend-Client-Primary-DNS', - 'auth_type' => 'Auth-Type', - 'ascend_secondary_home_ag' => 'Ascend-Secondary-Home-Agent', - 'x_ascend_idle_limit' => 'X-Ascend-Idle-Limit', - 'ms_ras_vendor' => 'MS-RAS-Vendor', - 'ascend_pre_input_packets' => 'Ascend-Pre-Input-Packets', - 'ascend_bridge' => 'Ascend-Bridge', - 'h323_redirect_number' => 'h323-redirect-number', - 'usr_simplified_mnp_level' => 'USR-Simplified-MNP-Levels', + 'bridge_grouq' => 'Bridge_Group', + 'h323_return_code' => 'h323-return-code', + 'annex_host_allow' => 'Annex-Host-Allow', + 'cvx_modem_end_recv_line_' => 'CVX-Modem-End-Recv-Line-Lvl', + 'sip_method' => 'Sip-Method', + 'x_ascend_require_auth' => 'X-Ascend-Require-Auth', + 'cvpn3000_sep_card_assign' => 'CVPN3000-SEP-Card-Assignment', + 'le_ipsec_deny_action' => 'LE-IPSec-Deny-Action', 'annex_edo' => 'Annex-EDO', - 'acc_nbns_server_sec' => 'Acc-Nbns-Server-Sec', - 'ascend_cbcp_trunk_group' => 'Ascend-CBCP-Trunk-Group', - 'x_ascend_data_svc' => 'X-Ascend-Data-Svc', - 'le_terminate_detail' => 'LE-Terminate-Detail', - 'acct_output_octets' => 'Acct-Output-Octets', - 'usr_calling_party_number' => 'USR-Calling-Party-Number', - 'x_ascend_dhcp_maximum_le' => 'X-Ascend-DHCP-Maximum-Leases', + 'acct_delay_time' => 'Acct-Delay-Time', + 'login_tcp_port' => 'Login-TCP-Port', + 'ascend_temporary_rtes' => 'Ascend-Temporary-Rtes', + 'versanet_termination_cau' => 'Versanet-Termination-Cause', + 'ascend_dialed_number' => 'Ascend-Dialed-Number', + 'cvpn3000_ipsec_authentic' => 'CVPN3000-IPSec-Authentication', + 'ascend_fr_dlci' => 'Ascend-FR-DLCI', + 'annex_modem_disc_reason' => 'Annex-Modem-Disc-Reason', + 'x_ascend_receive_secret' => 'X-Ascend-Receive-Secret', + 'usr_ospf_addressless_ind' => 'USR-OSPF-Addressless-Index', + 'usr_ip_default_route_opt' => 'USR-IP-Default-Route-Option', + 'char_noecho' => 'Char-Noecho', + 'redcreek_tunneled_search' => 'RedCreek-Tunneled-Search-List', + 'ascend_pri_number_type' => 'Ascend-PRI-Number-Type', + 'aat_ip_tos_apply_to' => 'AAT-IP-TOS-Apply-To', + 'x_ascend_modem_shelfno' => 'X-Ascend-Modem-ShelfNo', + 'prefix' => 'Prefix', + 'usr_rad_dvmrp_metric' => 'USR-Rad-Dvmrp-Metric', + 'x_ascend_call_attempt_li' => 'X-Ascend-Call-Attempt-Limit', + 'usr_ip_saa_filter' => 'USR-IP-SAA-Filter', + 'itk_prompt' => 'ITK-Prompt', + 'ascend_port_redir_protoc' => 'Ascend-Port-Redir-Protocol', + 'cvx_modem_tx_packets' => 'CVX-Modem-Tx-Packets', + 'usr_tunnel_switch_endpoi' => 'USR-Tunnel-Switch-Endpoint', + 'ascend_home_network_name' => 'Ascend-Home-Network-Name', + 'acc_customer_id' => 'Acc-Customer-Id', + 'message_authenticator' => 'Message-Authenticator', + 'cisco_fax_coverpage_flag' => 'Cisco-Fax-Coverpage-Flag', + 'usr_multicast_forwarding' => 'USR-Multicast-Forwarding', + 'cvpn3000_allow_network_e' => 'CVPN3000-Allow-Network-Extension-Mode', + 'ascend_call_direction' => 'Ascend-Call-Direction', + 'acc_connect_rx_speed' => 'Acc-Connect-Rx-Speed', 'ascend_force_56' => 'Ascend-Force-56', - 'shiva_acct_serv_switch' => 'Shiva-Acct-Serv-Switch', - 'tunnel_algorithm' => 'Tunnel_Algorithm', - 'usr_max_channels' => 'USR-Max-Channels', - 'usr_port_tap_priority' => 'USR-Port-Tap-Priority', - 'le_nat_outmap' => 'LE-NAT-Outmap', - 'usr_call_connecting_time' => 'USR-Call-Connecting-Time', - 'usr_supports_tags' => 'USR-Supports-Tags', - 'idle_timeout' => 'Idle-Timeout', - 'usr_ip_rip_input_filter' => 'USR-IP-RIP-Input-Filter', - 'erx_ingress_policy_name' => 'ERX-Ingress-Policy-Name', - 'usr_pw_cutoff' => 'USR-PW_Cutoff', - 'usr_channel_expansion' => 'USR-Channel-Expansion', - 'x_ascend_send_secret' => 'X-Ascend-Send-Secret', - 'h323_call_origin' => 'h323-call-origin', - 'h323_preferred_lang' => 'h323-preferred-lang', - 'ascend_base_channel_coun' => 'Ascend-Base-Channel-Count', - 'bind_auth_context' => 'Bind_Auth_Context', - 'ascend_calling_id_number' => 'Ascend-Calling-Id-Number-Plan', - 'ascend_modem_shelfno' => 'Ascend-Modem-ShelfNo', - 'tunnel_police_burst' => 'Tunnel_Police_Burst', - 'pvc_circuit_padding' => 'PVC_Circuit_Padding', - 'acc_ml_call_threshold' => 'Acc-ML-Call-Threshold', - 'usr_end_time' => 'USR-End-Time', - 'usr_ipx' => 'USR-IPX', - 'ms_primary_dns_server' => 'MS-Primary-DNS-Server', - 'ascend_dsl_upstream_limi' => 'Ascend-Dsl-Upstream-Limit', - 'usr_blocks_sent' => 'USR-Blocks-Sent', - 'bind_dot1q_vlan_tag_id' => 'Bind_Dot1q_Vlan_Tag_Id', - 'ascend_private_route' => 'Ascend-Private-Route', - 'usr_back_channel_data_ra' => 'USR-Back-Channel-Data-Rate', - 'ascend_dropped_packets' => 'Ascend-Dropped-Packets', - 'cisco_route_ip' => 'Cisco-Route-IP', - 'nas_identifier' => 'NAS-Identifier', - 'ascend_presession_time' => 'Ascend-PreSession-Time', - 'usr_call_type' => 'USR-Call-Type', - 'usr_acct_reason_code' => 'USR-Acct-Reason-Code', - 'acc_dialout_auth_passwor' => 'Acc-Dialout-Auth-Password', - 'acc_connect_tx_speed' => 'Acc-Connect-Tx-Speed', - 'cisco_pre_input_octets' => 'Cisco-Pre-Input-Octets', - 'x_ascend_send_passwd' => 'X-Ascend-Send-Passwd', - 'ascend_bir_bridge_group' => 'Ascend-BIR-Bridge-Group', - 'ascend_fr_profile_name' => 'Ascend-FR-Profile-Name', + 'st_service_domain' => 'ST-Service-Domain', + 'usr_harc_disconnect_code' => 'USR-HARC-Disconnect-Code', + 'shasta_service_profile' => 'Shasta-Service-Profile', + 'cisco_maximum_time' => 'Cisco-Maximum-Time', + 'usr_tunnel_auth_hostname' => 'USR-Tunnel-Auth-Hostname', + 'acc_ip_gateway_pri' => 'Acc-Ip-Gateway-Pri', + 'ascend_bridge_address' => 'Ascend-Bridge-Address', + 'altiga_pptp_min_authenti' => 'Altiga-PPTP-Min-Authentication-G/U', + 'ns_secondary_wins' => 'NS-Secondary-WINS', + 'cbbsm_bandwidth' => 'CBBSM-Bandwidth', + 'x_ascend_fr_link_mgt' => 'X-Ascend-FR-Link-Mgt', + 'altiga_ipsec_banner_g' => 'Altiga-IPSec-Banner-G', + 'ascend_handle_ipx' => 'Ascend-Handle-IPX', + 'ascend_x25_pad_alias_2' => 'Ascend-X25-Pad-Alias-2', + 'st_policy_name' => 'ST-Policy-Name', 'ascend_group' => 'Ascend-Group', - 'crypt_password' => 'Crypt-Password', - 'usr_port_tap_address' => 'USR-Port-Tap-Address', - 'le_nat_outsource_outmap' => 'LE-NAT-Outsource-Outmap', + 'ascend_dsl_rate_type' => 'Ascend-Dsl-Rate-Type', + 'tunnel_contexu' => 'Tunnel_Context', + 'ascend_require_auth' => 'Ascend-Require-Auth', + 'cvx_modem_local_retrains' => 'CVX-Modem-Local-Retrains', + 'cvpn5000_echo' => 'CVPN5000-Echo', + 'cvx_secondary_dns' => 'CVX-Secondary-DNS', + 'x_ascend_billing_number' => 'X-Ascend-Billing-Number', + 'usr_orig_nas_type' => 'USR-Orig-NAS-Type', + 'ascend_remote_fw' => 'Ascend-Remote-FW', + 'acct_output_packets' => 'Acct-Output-Packets', + 'lm_password' => 'LM-Password', + 'tunnel_window' => 'Tunnel-Window', + 'cisco_avpair' => 'Cisco-AVPair', + 'st_service_name' => 'ST-Service-Name', + 'shiva_event_flags' => 'Shiva-Event-Flags', + 'annex_retrain_requests_s' => 'Annex-Retrain-Requests-Sent', + 'ascend_ts_idle_mode' => 'Ascend-TS-Idle-Mode', + 'usr_ip_rip_simple_auth_p' => 'USR-IP-RIP-Simple-Auth-Password', + 'tunnel_deadtimf' => 'Tunnel_Deadtime', + 'state' => 'State', + 'usr_keypress_timeout' => 'USR-Keypress-Timeout', + 'usr_pw_vpn_neighbor' => 'USR-PW_VPN_Neighbor', + 'erx_pppoe_description' => 'ERX-Pppoe-Description', + 'ldap_userdn' => 'Ldap-UserDn', + 'x_ascend_fr_n391' => 'X-Ascend-FR-N391', + 'ascend_calling_id_presen' => 'Ascend-Calling-Id-Presentatn', + 'erx_local_loopback_inter' => 'ERX-Local-Loopback-Interface', + 'x_ascend_fr_direct' => 'X-Ascend-FR-Direct', + 'nas_ip_address' => 'NAS-IP-Address', + 'usr_call_end_time' => 'USR-Call-End-Time', + 'acct_mcast_out_packett' => 'Acct_Mcast_Out_Packets', + 'tunnel_algorithm' => 'Tunnel-Algorithm', 'usr_vpn_encrypter' => 'USR-VPN-Encrypter', - 'usr_blocks_received' => 'USR-Blocks-Received', - 'tunnel_group' => 'Tunnel_Group', - 'ascend_shared_profile_en' => 'Ascend-Shared-Profile-Enable', - 'replicate_to_realm' => 'Replicate-To-Realm', + 'tunnel_grouq' => 'Tunnel_Group', + 'ascend_atm_connect_group' => 'Ascend-ATM-Connect-Group', + 'x_ascend_ft1_caller' => 'X-Ascend-FT1-Caller', + 'usr_dnis_reauthenticatio' => 'USR-DNIS-ReAuthentication', + 'login_callback_number' => 'Login-Callback-Number', + 'usr_ip_rip_input_filter' => 'USR-IP-RIP-Input-Filter', + 'usr_rmmie_rcv_pwrlvl_330' => 'USR-RMMIE-Rcv-PwrLvl-3300Hz', + 'h323_disconnect_cause' => 'h323-disconnect-cause', + 'x_ascend_handle_ipx' => 'X-Ascend-Handle-IPX', + 'usr_igmp_version' => 'USR-IGMP-Version', + 'usr_imsi' => 'USR-IMSI', + 'group_name' => 'Group-Name', + 'usr_nas_type' => 'USR-NAS-Type', + 'context_namf' => 'Context-Name', + 'ascend_ip_tos' => 'Ascend-IP-TOS', + 'x_ascend_token_immediate' => 'X-Ascend-Token-Immediate', + 'tunnel_session_auth_serw' => 'Tunnel_Session_Auth_Service_Grp', + 'ms_chap2_cpw' => 'MS-CHAP2-CPW', + 'tunnel_session_auth_ctx' => 'Tunnel-Session-Auth-Ctx', + 'usr_mobile_numbytes_rxed' => 'USR-Mobile-NumBytes-Rxed', + 'usr_mbi_ct_tdm_time_slot' => 'USR-Mbi_Ct_TDM_Time_Slot', + 'ascend_x25_nui' => 'Ascend-X25-Nui', + 'x_ascend_first_dest' => 'X-Ascend-First-Dest', + 'usr_send_password' => 'USR-Send-Password', + 'x_ascend_fr_direct_profi' => 'X-Ascend-FR-Direct-Profile', + 'x_ascend_fr_t391' => 'X-Ascend-FR-T391', + 'altiga_ipsec_sec_associa' => 'Altiga-IPSec-Sec-Association-G/U', + 'ip_address_pool_namf' => 'Ip_Address_Pool_Name', + 'acct_input_octets' => 'Acct-Input-Octets', + 'cvx_modem_begin_modulati' => 'CVX-Modem-Begin-Modulation', + 'wispr_session_terminatea' => 'WISPr-Session-Terminate-End-Of-Day', + 'cvpn3000_use_client_addr' => 'CVPN3000-Use-Client-Address', + 'bridge_group' => 'Bridge-Group', + 'annex_sec_profile_index' => 'Annex-Sec-Profile-Index', + 'acc_dns_server_pri' => 'Acc-Dns-Server-Pri', + 'ms_acct_auth_type' => 'MS-Acct-Auth-Type', + 'x_ascend_maximum_call_du' => 'X-Ascend-Maximum-Call-Duration', + 'tunnel_password' => 'Tunnel-Password', + 'framed_ipv6_prefix' => 'Framed-IPv6-Prefix', + 'usr_reply_script5' => 'USR-Reply-Script5', + 'shiva_links_in_bundle' => 'Shiva-Links-In-Bundle', + 'ascend_fr_profile_name' => 'Ascend-FR-Profile-Name', + 'ascend_mtu' => 'Ascend-MTU', + 'nokia_charging_id' => 'Nokia-Charging-Id', + 'cvpn3000_ms_client_subne' => 'CVPN3000-MS-Client-Subnet-Mask', + 'cvpn3000_ipsec_sec_assoc' => 'CVPN3000-IPSec-Sec-Association', + 'cisco_ppp_async_map' => 'Cisco-PPP-Async-Map', + 'cvpn3000_user_auth_servf' => 'CVPN3000-User-Auth-Server-Port', + 'cisco_num_in_multilink' => 'Cisco-Num-In-Multilink', + 'wispr_logoff_url' => 'WISPr-Logoff-URL', 'usr_mobile_ip_address' => 'USR-Mobile-IP-Address', - 'x_ascend_authen_alias' => 'X-Ascend-Authen-Alias', - 'ascend_fr_linkup' => 'Ascend-FR-LinkUp', - 'tunnel_rate_limit_rate' => 'Tunnel_Rate_Limit_Rate', - 'acc_access_community' => 'Acc-Access-Community', + 'usr_final_tx_link_data_r' => 'USR-Final-Tx-Link-Data-Rate', + 'itk_ppp_compression_prot' => 'ITK-PPP-Compression-Prot', + 'ascend_bridge' => 'Ascend-Bridge', 'x_ascend_presession_time' => 'X-Ascend-PreSession-Time', - 'ms_chap_cpw_1' => 'MS-CHAP-CPW-1', - 'ms_chap_cpw_2' => 'MS-CHAP-CPW-2', - 'erx_primary_dns' => 'ERX-Primary-Dns', - 'ascend_fr_circuit_name' => 'Ascend-FR-Circuit-Name', - 'ascend_token_immediate' => 'Ascend-Token-Immediate', - 'cisco_idle_limit' => 'Cisco-Idle-Limit', + 'aat_client_primary_dns' => 'AAT-Client-Primary-DNS', + 'cvpn3000_strip_realm' => 'CVPN3000-Strip-Realm', + 'tunnel_cmd_timeout' => 'Tunnel-Cmd-Timeout', + 'ascend_multicast_client' => 'Ascend-Multicast-Client', + 'cvx_modem_remote_rate_ne' => 'CVX-Modem-Remote-Rate-Negs', + 'tunnel_private_group_id' => 'Tunnel-Private-Group-Id', + 'usr_rmmie_rcv_tot_pwrlvl' => 'USR-RMMIE-Rcv-Tot-PwrLvl', + 'calling_station_id' => 'Calling-Station-Id', + 'tunnel_rate_limit_burst' => 'Tunnel-Rate-Limit-Burst', + 'usr_device_connected_to' => 'USR-Device-Connected-To', + 'aat_source_ip_check' => 'AAT-Source-IP-Check', + 'login_lat_service' => 'Login-LAT-Service', + 'ascend_h323_fegw_address' => 'Ascend-H323-Fegw-Address', + 'usr_called_party_number' => 'USR-Called-Party-Number', + 'bintec_ipnatpresettable' => 'BinTec-ipNatPresetTable', + 'ascend_remove_seconds' => 'Ascend-Remove-Seconds', + 'shiva_user_attributes' => 'Shiva-User-Attributes', + 'cisco_fax_dsn_flag' => 'Cisco-Fax-Dsn-Flag', + 'x_ascend_route_ipx' => 'X-Ascend-Route-IPX', + 'acc_route_policy' => 'Acc-Route-Policy', + 'bind_l2tp_flow_controm' => 'Bind_L2TP_Flow_Control', + 'erx_qos_profile_name' => 'ERX-Qos-Profile-Name', + 'x_ascend_client_gateway' => 'X-Ascend-Client-Gateway', + 'pre_proxy_type' => 'Pre-Proxy-Type', + 'smb_account_ctrl_text' => 'SMB-Account-CTRL-TEXT', + 'x_ascend_data_filter' => 'X-Ascend-Data-Filter', + 'usr_rmmie_last_update_ti' => 'USR-RMMIE-Last-Update-Time', + 'ascend_atm_direct' => 'Ascend-ATM-Direct', + 'ascend_session_type' => 'Ascend-Session-Type', + 'x_ascend_fr_linkup' => 'X-Ascend-FR-LinkUp', + 'ascend_metric' => 'Ascend-Metric', + 'x_ascend_assign_ip_clien' => 'X-Ascend-Assign-IP-Client', 'usr_speed_of_connection' => 'USR-Speed-Of-Connection', - 'shiva_links_in_bundle' => 'Shiva-Links-In-Bundle', - 'x_ascend_fr_profile_name' => 'X-Ascend-FR-Profile-Name', - 'cisco_multilink_id' => 'Cisco-Multilink-ID', - 'x_ascend_preempt_limit' => 'X-Ascend-Preempt-Limit', - 'ascend_assign_ip_client' => 'Ascend-Assign-IP-Client', - 'usr_iwf_ip_address' => 'USR-IWF-IP-Address', + 'cvpn3000_require_hw_clie' => 'CVPN3000-Require-HW-Client-Auth', + 'session_type' => 'Session-Type', + 'acct_input_octets_65' => 'Acct_Input_Octets_64', + 'le_nat_outsource_outmap' => 'LE-NAT-Outsource-Outmap', + 'cvx_modem_local_rate_neg' => 'CVX-Modem-Local-Rate-Negs', + 'mcast_sene' => 'Mcast_Send', + 'pppoe_url' => 'PPPOE-URL', + 'erx_service_bundle' => 'ERX-Service-Bundle', + 'altiga_secondary_dns_g' => 'Altiga-Secondary-DNS-G', + 'bg_trans_bpdv' => 'BG_Trans_BPDU', + 'cvx_data_filter' => 'CVX-Data-Filter', + 'acct_mcast_out_octets' => 'Acct-Mcast-Out-Octets', + 'ascend_callback' => 'Ascend-Callback', + 'tunnel_client_auth_id' => 'Tunnel-Client-Auth-Id', 'acct_unique_session_id' => 'Acct-Unique-Session-Id', - 'framed_pool' => 'Framed-Pool', - 'usr_igmp_version' => 'USR-IGMP-Version', - 'tunnel_max_tunnels' => 'Tunnel_Max_Tunnels', - 'annex_unauthenticated_ti' => 'Annex-Unauthenticated-Time', - 'bg_path_cost' => 'BG_Path_Cost', - 'ascend_client_assign_win' => 'Ascend-Client-Assign-WINS', - 'x_ascend_dial_number' => 'X-Ascend-Dial-Number', - 'cisco_maximum_channels' => 'Cisco-Maximum-Channels', - 'usr_pw_framed_routing_v2' => 'USR-PW_Framed_Routing_V2', - 'usr_channel_decrement' => 'USR-Channel-Decrement', - 'x_ascend_route_ipx' => 'X-Ascend-Route-IPX', + 'usr_port_tap_format' => 'USR-Port-Tap-Format', + 'ascend_ckt_type' => 'Ascend-Ckt-Type', + 'ascend_ppp_async_map' => 'Ascend-PPP-Async-Map', + 'usr_rmmie_rcv_pwrlvl_375' => 'USR-RMMIE-Rcv-PwrLvl-3750Hz', + 'usr_acct_reason_code' => 'USR-Acct-Reason-Code', + 'ascend_filter' => 'Ascend-Filter', + 'h323_redirect_number' => 'h323-redirect-number', 'port_limit' => 'Port-Limit', - 'ascend_dsl_downstream_li' => 'Ascend-Dsl-Downstream-Limit', - 'ascend_ip_tos_precedence' => 'Ascend-IP-TOS-Precedence', - 'usr_multicast_receive' => 'USR-Multicast-Receive', - 'usr_auth_mode' => 'USR-Auth-Mode', + 'rewrite_rule' => 'Rewrite-Rule', + 'tunnel_police_rate' => 'Tunnel-Police-Rate', + 'usr_multicast_proxy' => 'USR-Multicast-Proxy', + 'ascend_max_shared_users' => 'Ascend-Max-Shared-Users', + 'usr_bridging' => 'USR-Bridging', + 'cvx_presession_time' => 'CVX-PreSession-Time', + 'cvpn5000_vpn_groupinfo' => 'CVPN5000-VPN-GroupInfo', + 'autz_type' => 'Autz-Type', + 'x_ascend_fr_dlci' => 'X-Ascend-FR-DLCI', + 'usr_request_type' => 'USR-Request-Type', + 'acc_igmp_admin_state' => 'Acc-Igmp-Admin-State', + 'ascend_host_info' => 'Ascend-Host-Info', + 'ascend_dhcp_maximum_leas' => 'Ascend-DHCP-Maximum-Leases', + 'usr_rmmie_num_of_updates' => 'USR-RMMIE-Num-Of-Updates', + 'x_ascend_fr_profile_name' => 'X-Ascend-FR-Profile-Name', + 'ascend_fr_direct_profile' => 'Ascend-FR-Direct-Profile', + 'x_ascend_bridge' => 'X-Ascend-Bridge', + 'tunnel_deadtime' => 'Tunnel-Deadtime', + 'ms_chap_error' => 'MS-CHAP-Error', + 'framed_route' => 'Framed-Route', + 'sip_from' => 'Sip-From', 'expiration' => 'Expiration', - 'x_ascend_fr_circuit_name' => 'X-Ascend-FR-Circuit-Name', - 'x_ascend_token_immediate' => 'X-Ascend-Token-Immediate', - 'ascend_ft1_caller' => 'Ascend-FT1-Caller', - 'shiva_event_flags' => 'Shiva-Event-Flags', - 'framed_netmask' => 'Framed-Netmask', - 'ascend_minimum_channels' => 'Ascend-Minimum-Channels', - 'acc_ml_damping_factor' => 'Acc-ML-Damping-Factor', - 'bind_sub_password' => 'Bind_Sub_Password', - 'ascend_ip_tos_apply_to' => 'Ascend-IP-TOS-Apply-To', - 'x_ascend_home_agent_udp_' => 'X-Ascend-Home-Agent-UDP-Port', - 'x_ascend_menu_item' => 'X-Ascend-Menu-Item', - 'ascend_session_type' => 'Ascend-Session-Type', - 'usr_pw_packet' => 'USR-PW_Packet', - 'session' => 'Session', - 'usr_mic' => 'USR-MIC', - 'usr_line_reversals' => 'USR-Line-Reversals', - 'assigned_ip_address' => 'Assigned_IP_Address', - 'cisco_ip_direct' => 'Cisco-IP-Direct', - 'le_ipsec_log_options' => 'LE-IPSec-Log-Options', - 'tunnel_rate_limit_burst' => 'Tunnel_Rate_Limit_Burst', - 'x_ascend_assign_ip_globa' => 'X-Ascend-Assign-IP-Global-Pool', + 'ascend_backup' => 'Ascend-Backup', + 'ascend_pre_output_octets' => 'Ascend-Pre-Output-Octets', + 'ascend_calling_id_number' => 'Ascend-Calling-Id-Number-Plan', + 'framed_appletalk_zone' => 'Framed-AppleTalk-Zone', + 'annex_audit_level' => 'Annex-Audit-Level', + 'digest_algorithm' => 'Digest-Algorithm', + 'bind_auth_context' => 'Bind-Auth-Context', + 'ascend_user_acct_base' => 'Ascend-User-Acct-Base', + 'st_secondary_dns_server' => 'ST-Secondary-DNS-Server', + 'mcast_receive' => 'Mcast-Receive', + 'usr_ds0' => 'USR-DS0', + 'aat_atm_traffic_profile' => 'AAT-ATM-Traffic-Profile', + 'ms_ras_vendor' => 'MS-RAS-Vendor', + 'tunnel_domain' => 'Tunnel-Domain', + 'tunnel_max_sessions' => 'Tunnel-Max-Sessions', + 'ascend_ip_direct' => 'Ascend-IP-Direct', + 'xedia_address_pool' => 'Xedia-Address-Pool', + 'idle_timeout' => 'Idle-Timeout', + 'tunnel_rate_limit_ratf' => 'Tunnel_Rate_Limit_Rate', + 'annex_rate_reneg_req_sen' => 'Annex-Rate-Reneg-Req-Sent', + 'usr_initial_tx_link_data' => 'USR-Initial-Tx-Link-Data-Rate', + 'tunnel_server_auth_id' => 'Tunnel-Server-Auth-Id', + 'cvpn3000_ipsec_banner1' => 'CVPN3000-IPSec-Banner1', + 'usr_start_time' => 'USR-Start-Time', + 'usr_ip' => 'USR-IP', + 'cvpn3000_reqrd_client_fw' => 'CVPN3000-Reqrd-Client-Fw-Vendor-Code', + 'altiga_ipsec_secondary_d' => 'Altiga-IPSec-Secondary-Domains-G', + 'usr_gateway_ip_address' => 'USR-Gateway-IP-Address', + 'ascend_dba_monitor' => 'Ascend-DBA-Monitor', + 'ms_link_utilization_thre' => 'MS-Link-Utilization-Threshold', + 'st_primary_dns_server' => 'ST-Primary-DNS-Server', + 'acc_ace_token_ttl' => 'Acc-Ace-Token-Ttl', + 'ms_chap_domain' => 'MS-CHAP-Domain', + 'cisco_pre_input_octets' => 'Cisco-Pre-Input-Octets', + 'ascend_primary_home_agen' => 'Ascend-Primary-Home-Agent', + 'acct_session_time' => 'Acct-Session-Time', + 'framed_ip_address' => 'Framed-IP-Address', + 'ns_admin_privilege' => 'NS-Admin-Privilege', + 'medium_type' => 'Medium-Type', + 'acct_output_octets_64' => 'Acct-Output-Octets-64', + 'ascend_cir_timer' => 'Ascend-CIR-Timer', + 'police_rate' => 'Police-Rate', + 'tunnel_functioo' => 'Tunnel_Function', + 'quintum_h323_time_and_da' => 'Quintum-h323-time-and-day', + 'ip_tos_fiele' => 'IP_TOS_Field', + 'erx_framed_ip_route_tag' => 'ERX-Framed-Ip-Route-Tag', + 'ms_mppe_send_key' => 'MS-MPPE-Send-Key', + 'ascend_maximum_call_dura' => 'Ascend-Maximum-Call-Duration', + 'pppoe_motn' => 'PPPOE_MOTM', + 'lac_poru' => 'LAC_Port', + 'bind_dot1q_slou' => 'Bind_Dot1q_Slot', + 'ascend_secondary_home_ag' => 'Ascend-Secondary-Home-Agent', + 'usr_ip_call_output_filte' => 'USR-IP-Call-Output-Filter', + 'x_ascend_host_info' => 'X-Ascend-Host-Info', + 'erx_egress_policy_name' => 'ERX-Egress-Policy-Name', + 'erx_ppp_password' => 'ERX-PPP-Password', + 'user_name' => 'User-Name', + 'usr_number_of_characters' => 'USR-Number-Of-Characters-Lost', + 'bind_bypass_bypass' => 'Bind-Bypass-Bypass', + 'usr_rad_multicast_routip' => 'USR-Rad-Multicast-Routing-Proto', + 'annex_acct_servers' => 'Annex-Acct-Servers', + 'cvpn5000_tunnel_throughp' => 'CVPN5000-Tunnel-Throughput', + 'usr_chassis_call_channel' => 'USR-Chassis-Call-Channel', + 'annex_input_filter' => 'Annex-Input-Filter', + 'wispr_billing_class_of_s' => 'WISPr-Billing-Class-Of-Service', + 'nas_port_type' => 'NAS-Port-Type', + 'cvx_client_assign_dns' => 'CVX-Client-Assign-DNS', + 'nomadix_maxbytesdown' => 'Nomadix-MaxBytesDown', + 'ascend_endpoint_disc' => 'Ascend-Endpoint-Disc', + 'tunnel_police_burst' => 'Tunnel-Police-Burst', + 'bind_auth_max_sessions' => 'Bind-Auth-Max-Sessions', + 'cvx_identification' => 'CVX-Identification', + 'cvpn3000_ipsec_allow_pas' => 'CVPN3000-IPSec-Allow-Passwd-Store', + 'ascend_calling_id_type_o' => 'Ascend-Calling-Id-Type-Of-Num', + 'x_ascend_fr_dce_n392' => 'X-Ascend-FR-DCE-N392', + 'usr_connect_term_reason' => 'USR-Connect-Term-Reason', + 'erx_egress_statistics' => 'ERX-Egress-Statistics', + 'ascend_fr_dte_n392' => 'Ascend-FR-DTE-N392', + 'usr_esn' => 'USR-ESN', + 'x_ascend_fr_dte_n392' => 'X-Ascend-FR-DTE-N392', + 'itk_modem_init_string' => 'ITK-Modem-Init-String', + 'x_ascend_fr_nailed_grp' => 'X-Ascend-FR-Nailed-Grp', + 'ascend_bridge_non_pppoe' => 'Ascend-Bridge-Non-PPPoE', + 'cvpn3000_ipsec_reqrd_cli' => 'CVPN3000-IPSec-Reqrd-Client-Fw-Cap', + 'ascend_ipx_alias' => 'Ascend-IPX-Alias', + 'acc_tunnel_port' => 'Acc-Tunnel-Port', + 'quintum_h323_return_code' => 'Quintum-h323-return-code', + 'cvpn3000_l2tp_encryption' => 'CVPN3000-L2TP-Encryption', + 'acct_input_gigawords' => 'Acct-Input-Gigawords', + 'bind_dot1q_poru' => 'Bind_Dot1q_Port', + 'altiga_primary_wins_g' => 'Altiga-Primary-WINS-G', + 'ascend_maximum_channels' => 'Ascend-Maximum-Channels', + 'x_ascend_home_agent_pass' => 'X-Ascend-Home-Agent-Password', + 'x_ascend_ppp_async_map' => 'X-Ascend-PPP-Async-Map', + 'usr_rmmie_manufacturer_i' => 'USR-RMMIE-Manufacturer-ID', + 'usr_retrains_requested' => 'USR-Retrains-Requested', + 'x_ascend_metric' => 'X-Ascend-Metric', + 'acc_apsm_oversubscribed' => 'Acc-Apsm-Oversubscribed', + 'usr_originate_answer_mod' => 'USR-Originate-Answer-Mode', + 'erx_atm_pcr' => 'ERX-Atm-PCR', + 'itk_nas_name' => 'ITK-NAS-Name', + 'usr_ipx_routing' => 'USR-IPX-Routing', + 'usr_tunneled_mlpp' => 'USR-Tunneled-MLPP', + 'usr_send_script5' => 'USR-Send-Script5', + 'ascend_traffic_shaper' => 'Ascend-Traffic-Shaper', + 'ascend_client_secondarya' => 'Ascend-Client-Secondary-DNS', + 'ascend_bacp_enable' => 'Ascend-BACP-Enable', + 'usr_call_terminate_in_gm' => 'USR-Call-Terminate-in-GMT', + 'login_time' => 'Login-Time', + 'bg_path_cosu' => 'BG_Path_Cost', + 'aat_require_auth' => 'AAT-Require-Auth', + 'cvpn3000_reqrd_client_fy' => 'CVPN3000-Reqrd-Client-Fw-Description', + 'ascend_call_type' => 'Ascend-Call-Type', + 'erx_address_pool_name' => 'ERX-Address-Pool-Name', + 'cvpn3000_ipsec_backup_sf' => 'CVPN3000-IPSec-Backup-Server-List', + 'h323_incoming_conf_id' => 'h323-incoming-conf-id', + 'user_profile' => 'User-Profile', + 'ip_host_adds' => 'Ip_Host_Addr', + 'ns_primary_wins' => 'NS-Primary-WINS', + 'packet_type' => 'Packet-Type', + 'bind_auth_max_sessiont' => 'Bind_Auth_Max_Sessions', + 'altiga_allow_alpha_only_' => 'Altiga-Allow-Alpha-Only-Passwords-G', + 'usr_security_resp_limit' => 'USR-Security-Resp-Limit', + 'ip_address_pool_name' => 'Ip-Address-Pool-Name', + 'ascend_ipx_node_addr' => 'Ascend-IPX-Node-Addr', + 'ascend_cbcp_trunk_group' => 'Ascend-CBCP-Trunk-Group', + 'ascend_menu_selector' => 'Ascend-Menu-Selector', + 'ascend_assign_ip_global_' => 'Ascend-Assign-IP-Global-Pool', + 'usr_ds0s' => 'USR-DS0s', + 'usr_actual_voltage' => 'USR-Actual-Voltage', + 'quintum_h323_call_type' => 'Quintum-h323-call-type', + 'annex_sw_version' => 'Annex-SW-Version', + 'ascend_receive_secret' => 'Ascend-Receive-Secret', + 'bintec_qospolicytable' => 'BinTec-qosPolicyTable', + 'usr_ip_rip_policies' => 'USR-IP-RIP-Policies', + 'redcreek_tunneled_ip_add' => 'RedCreek-Tunneled-IP-Addr', + 'ascend_pw_warntime' => 'Ascend-PW-Warntime', 'x_ascend_inc_channel_cou' => 'X-Ascend-Inc-Channel-Count', - 'h323_return_code' => 'h323-return-code', - 'shiva_disconnect_reason' => 'Shiva-Disconnect-Reason', - 'filter_id' => 'Filter-Id', - 'usr_appletalk_network_ra' => 'USR-Appletalk-Network-Range', - 'ascend_temporary_rtes' => 'Ascend-Temporary-Rtes', - 'ascend_h323_conference_i' => 'Ascend-H323-Conference-Id', + 'usr_blocks_resent' => 'USR-Blocks-Resent', + 'usr_fallback_enabled' => 'USR-Fallback-Enabled', + 'arap_challenge_response' => 'ARAP-Challenge-Response', + 'tunnel_session_auth' => 'Tunnel-Session-Auth', + 'usr_sync_async_mode' => 'USR-Sync-Async-Mode', + 'itk_dialout_type' => 'ITK-Dialout-Type', + 'extreme_netlogin_url' => 'Extreme-Netlogin-Url', + 'client_port_dnis' => 'Client-Port-DNIS', + 'digest_realm' => 'Digest-Realm', + 'ascend_ppp_vj_1172' => 'Ascend-PPP-VJ-1172', + 'ascend_fr_n391' => 'Ascend-FR-N391', + 'ascend_remote_addr' => 'Ascend-Remote-Addr', + 'client_port_id' => 'Client-Port-Id', + 'digest_body_digest' => 'Digest-Body-Digest', + 'le_ipsec_active_profile' => 'LE-IPSec-Active-Profile', + 'digest_cnonce' => 'Digest-CNonce', + 'usr_port_tap_facility' => 'USR-Port-Tap-Facility', + 'usr_callback_type' => 'USR-Callback-Type', + 'client_dns_prj' => 'Client_DNS_Pri', + 'digest_response' => 'Digest-Response', + 'login_lat_group' => 'Login-LAT-Group', + 'x_ascend_call_type' => 'X-Ascend-Call-Type', + 'ascend_route_ip' => 'Ascend-Route-IP', + 'usr_rad_multicast_routio' => 'USR-Rad-Multicast-Routing-RtLim', + 'usr_pw_vpn_id' => 'USR-PW_VPN_ID', + 'cvx_modem_end_modulation' => 'CVX-Modem-End-Modulation', + 'cvpn3000_pptp_mppc_compr' => 'CVPN3000-PPTP-MPPC-Compression', + 'cisco_pre_output_octets' => 'Cisco-Pre-Output-Octets', 'h323_billing_model' => 'h323-billing-model', - 'usr_bearer_capabilities' => 'USR-Bearer-Capabilities', - 'framed_appletalk_zone' => 'Framed-AppleTalk-Zone', - 'usr_harc_disconnect_code' => 'USR-HARC-Disconnect-Code', + 'usr_equalization_type' => 'USR-Equalization-Type', + 'acc_clearing_cause' => 'Acc-Clearing-Cause', + 'altiga_access_hours_g_u' => 'Altiga-Access-Hours-G/U', + 'cvpn3000_ipsec_user_grou' => 'CVPN3000-IPSec-User-Group-Lock', + 'x_ascend_menu_selector' => 'X-Ascend-Menu-Selector', + 'x_ascend_netware_timeout' => 'X-Ascend-Netware-timeout', + 'ascend_fr_linkup' => 'Ascend-FR-LinkUp', + 'annex_num_in_multilink' => 'Annex-Num-In-Multilink', + 'police_burst' => 'Police-Burst', + 'altiga_l2tp_min_authenti' => 'Altiga-L2TP-Min-Authentication-G/U', + 'ascend_filter_required' => 'Ascend-Filter-Required', + 'x_ascend_idle_limit' => 'X-Ascend-Idle-Limit', + 'nomadix_logoff_url' => 'Nomadix-Logoff-URL', + 'cvpn3000_ms_client_icpt_' => 'CVPN3000-MS-Client-Icpt-DHCP-Conf-Msg', + 'ip_tos_field' => 'IP-TOS-Field', + 'ascend_ip_tos_apply_to' => 'Ascend-IP-TOS-Apply-To', + 'usr_call_event_code' => 'USR-Call-Event-Code', + 'usr_et_bridge_output_fil' => 'USR-ET-Bridge-Output-Filter', + 'le_nat_sess_dir_fail_act' => 'LE-NAT-Sess-Dir-Fail-Action', + 'usr_rmmie_product_code' => 'USR-RMMIE-Product-Code', + 'usr_host_type' => 'USR-Host-Type', + 'erx_tunnel_interface_id' => 'ERX-Tunnel-Interface-Id', + 'ascend_send_auth' => 'Ascend-Send-Auth', + 'shiva_compression_type' => 'Shiva-Compression-Type', + 'itk_banner' => 'ITK-Banner', + 'ascend_ft1_caller' => 'Ascend-FT1-Caller', + 'filter_id' => 'Filter-Id', + 'annex_pre_output_octets' => 'Annex-Pre-Output-Octets', + 'acct_mcast_in_octett' => 'Acct_Mcast_In_Octets', + 'usr_log_filter_packets' => 'USR-Log-Filter-Packets', + 'ascend_fr_nailed_grp' => 'Ascend-FR-Nailed-Grp', + 'ascend_atm_loopback_cell' => 'Ascend-ATM-Loopback-Cell-Loss', + 'usr_at_rtmp_output_filte' => 'USR-AT-RTMP-Output-Filter', + 'acc_input_errors' => 'Acc-Input-Errors', + 'x_ascend_user_acct_port' => 'X-Ascend-User-Acct-Port', + 'erx_secondary_wins' => 'ERX-Secondary-Wins', + 'usr_rmmie_serial_number' => 'USR-RMMIE-Serial-Number', + 'usr_et_bridge_input_filt' => 'USR-ET-Bridge-Input-Filter', + 'ns_primary_dns' => 'NS-Primary-DNS', + 'usr_slot_connected_to' => 'USR-Slot-Connected-To', + 'shiva_disconnect_reason' => 'Shiva-Disconnect-Reason', + 'cvpn5000_client_assignee' => 'CVPN5000-Client-Assigned-IPX', + 'cvx_radius_redirect' => 'CVX-Radius-Redirect', + 'usr_receive_acc_map' => 'USR-Receive-Acc-Map', + 'x_ascend_tunneling_proto' => 'X-Ascend-Tunneling-Protocol', + 'itk_acct_serv_ip' => 'ITK-Acct-Serv-IP', + 'ascend_fr_type' => 'Ascend-FR-Type', + 'ascend_client_assign_dns' => 'Ascend-Client-Assign-DNS', + 'annex_retrain_requests_r' => 'Annex-Retrain-Requests-Rcvd', + 'x_ascend_assign_ip_globa' => 'X-Ascend-Assign-IP-Global-Pool', + 'tunnel_client_endpoint' => 'Tunnel-Client-Endpoint', + 'alteon_service_type' => 'Alteon-Service-Type', + 'x_ascend_send_secret' => 'X-Ascend-Send-Secret', + 'x_ascend_call_filter' => 'X-Ascend-Call-Filter', 'usr_ipx_rip_input_filter' => 'USR-IPX-RIP-Input-Filter', - 'usr_rad_multicast_routin' => 'USR-Rad-Multicast-Routing-Bound', - 'ascend_pw_lifetime' => 'Ascend-PW-Lifetime', - 'acc_dialout_auth_usernam' => 'Acc-Dialout-Auth-Username', - 'ascend_x25_pad_x3_parame' => 'Ascend-X25-Pad-X3-Parameters', - 'bind_dot1q_slot' => 'Bind_Dot1q_Slot', - 'usr_rad_multicast_routin' => 'USR-Rad-Multicast-Routing-RtLim', - 'x_ascend_multicast_clien' => 'X-Ascend-Multicast-Client', - 'ascend_authen_alias' => 'Ascend-Authen-Alias', - 'ascend_dec_channel_count' => 'Ascend-Dec-Channel-Count', - 'dhcp_max_leases' => 'DHCP_Max_Leases', - 'shiva_called_number' => 'Shiva-Called-Number', - 'annex_tunnel_authen_mode' => 'Annex-Tunnel-Authen-Mode', - 'usr_call_error_code' => 'USR-Call-Error-Code', - 'x_ascend_user_acct_type' => 'X-Ascend-User-Acct-Type', - 'ascend_atm_connect_vpi' => 'Ascend-ATM-Connect-Vpi', - 'ascend_x25_pad_x3_profil' => 'Ascend-X25-Pad-X3-Profile', - 'usr_mobileip_home_agent_' => 'USR-MobileIP-Home-Agent-Address', - 'suffix' => 'Suffix', - 'bind_tun_context' => 'Bind_Tun_Context', - 'x_ascend_ppp_address' => 'X-Ascend-PPP-Address', - 'usr_dtr_false_timeout' => 'USR-DTR-False-Timeout', - 'usr_final_rx_link_data_r' => 'USR-Final-Rx-Link-Data-Rate', - 'ms_chap_error' => 'MS-CHAP-Error', - 'x_ascend_home_agent_ip_a' => 'X-Ascend-Home-Agent-IP-Addr', - 'ascend_data_svc' => 'Ascend-Data-Svc', - 'usr_rmmie_pwrlvl_noise_l' => 'USR-RMMIE-PwrLvl-Noise-Lvl', - 'usr_dtr_true_timeout' => 'USR-DTR-True-Timeout', - 'context_name' => 'Context-Name', - 'usr_card_type' => 'USR-Card-Type', - 'ascend_fr_link_status_dl' => 'Ascend-FR-Link-Status-DLCI', - 'annex_sec_profile_index' => 'Annex-Sec-Profile-Index', - 'usr_pw_usr_ofilter_sap' => 'USR-PW_USR_OFilter_SAP', - 'tunnel_medium_type' => 'Tunnel-Medium-Type', - 'x_ascend_require_auth' => 'X-Ascend-Require-Auth', - 'ascend_connect_progress' => 'Ascend-Connect-Progress', - 'x_ascend_modem_shelfno' => 'X-Ascend-Modem-ShelfNo', - 'cisco_pre_input_packets' => 'Cisco-Pre-Input-Packets', - 'ascend_fr_dce_n392' => 'Ascend-FR-DCE-N392', - 'ascend_fr_dce_n393' => 'Ascend-FR-DCE-N393', - 'ascend_client_primary_wi' => 'Ascend-Client-Primary-WINS', - 'shiva_link_protocol' => 'Shiva-Link-Protocol', - 'bridge_group' => 'Bridge_Group', - 'client_port_dnis' => 'Client-Port-DNIS', - 'usr_mpip_tunnel_originat' => 'USR-MPIP-Tunnel-Originator', - 'le_nat_log_options' => 'LE-NAT-Log-Options', + 'x_ascend_maximum_time' => 'X-Ascend-Maximum-Time', + 'pvc_profile_name' => 'PVC-Profile-Name', + 'usr_framed_ip_address_po' => 'USR-Framed_IP_Address_Pool_Name', + 'cvpn3000_ipsec_split_dns' => 'CVPN3000-IPSec-Split-DNS-Names', + 'ascend_global_call_id' => 'Ascend-Global-Call-Id', + 'usr_initial_rx_link_data' => 'USR-Initial-Rx-Link-Data-Rate', + 'st_primary_nbns_server' => 'ST-Primary-NBNS-Server', 'usr_number_of_rings_limi' => 'USR-Number-of-Rings-Limit', - 'usr_retrains_granted' => 'USR-Retrains-Granted', - 'acc_ip_gateway_pri' => 'Acc-Ip-Gateway-Pri', - 'usr_number_of_fallbacks' => 'USR-Number-of-Fallbacks', - 'usr_tunnel_auth_hostname' => 'USR-Tunnel-Auth-Hostname', - 'annex_filter' => 'Annex-Filter', - 'ascend_mtu' => 'Ascend-MTU', - 'ms_arap_pw_change_reason' => 'MS-ARAP-PW-Change-Reason', - 'private_group_id' => 'Private-Group-Id', - 'ascend_cache_time' => 'Ascend-Cache-Time', - 'acc_ml_clear_threshold' => 'Acc-ML-Clear-Threshold', - 'x_ascend_dhcp_reply' => 'X-Ascend-DHCP-Reply', - 'ascend_h323_gatekeeper' => 'Ascend-H323-Gatekeeper', - 'x_ascend_xmit_rate' => 'X-Ascend-Xmit-Rate', - 'usr_last_number_dialed_o' => 'USR-Last-Number-Dialed-Out', - 'acc_connect_rx_speed' => 'Acc-Connect-Rx-Speed', - 'acc_clearing_cause' => 'Acc-Clearing-Cause', - 'ascend_call_attempt_limi' => 'Ascend-Call-Attempt-Limit', - 'x_ascend_data_rate' => 'X-Ascend-Data-Rate', - 'termination_action' => 'Termination-Action', - 'ascend_pre_input_octets' => 'Ascend-Pre-Input-Octets', - 'x_ascend_ipx_route' => 'X-Ascend-IPX-Route', - 'x_ascend_ts_idle_mode' => 'X-Ascend-TS-Idle-Mode', - 'client_ip_address' => 'Client-IP-Address', - 'ascend_add_seconds' => 'Ascend-Add-Seconds', - 'login_ip_host' => 'Login-IP-Host', - 'annex_sw_version' => 'Annex-SW-Version', + 'tunnel_local_name' => 'Tunnel-Local-Name', + 'ascend_fr_t392' => 'Ascend-FR-T392', + 'annex_pool_id' => 'Annex-Pool-Id', + 'ascend_token_immediate' => 'Ascend-Token-Immediate', + 'usr_rmmie_firmware_build' => 'USR-RMMIE-Firmware-Build-Date', + 'wispr_bandwidth_min_down' => 'WISPr-Bandwidth-Min-Down', + 'usr_chassis_call_slot' => 'USR-Chassis-Call-Slot', + 'rate_limit_burst' => 'Rate-Limit-Burst', + 'cisco_route_ip' => 'Cisco-Route-IP', + 'xedia_netbios_server' => 'Xedia-NetBios-Server', + 'session_error_msg' => 'Session-Error-Msg', + 'dhcp_max_leases' => 'DHCP-Max-Leases', + 'acc_vpsm_reject_cause' => 'Acc-Vpsm-Reject-Cause', + 'user_category' => 'User-Category', + 'x_ascend_multicast_rate_' => 'X-Ascend-Multicast-Rate-Limit', + 'cvpn3000_ipsec_auth_on_r' => 'CVPN3000-IPSec-Auth-On-Rekey', + 'altiga_min_password_leng' => 'Altiga-Min-Password-Length-G', + 'bind_type' => 'Bind-Type', + 'ascend_tunneling_protoco' => 'Ascend-Tunneling-Protocol', + 'cvx_modem_retx_packets' => 'CVX-Modem-ReTx-Packets', + 'usr_framed_ipx_route' => 'USR-Framed-IPX-Route', + 'rate_limit_rate' => 'Rate-Limit-Rate', + 'ascend_atm_connect_vpi' => 'Ascend-ATM-Connect-Vpi', + 'connect_info' => 'Connect-Info', + 'usr_port_tap_address' => 'USR-Port-Tap-Address', + 'usr_simplified_mnp_level' => 'USR-Simplified-MNP-Levels', + 'mcast_receivf' => 'Mcast_Receive', + 'annex_begin_modulation' => 'Annex-Begin-Modulation', + 'usr_pw_usr_ifilter_ip' => 'USR-PW_USR_IFilter_IP', + 'ascend_route_appletalk' => 'Ascend-Route-Appletalk', + 'ms_chap_lm_enc_pw' => 'MS-CHAP-LM-Enc-PW', + 'altiga_ipsec_over_nat_po' => 'Altiga-IPSec-Over-NAT-Port-Num-G', + 'itk_isdn_prot' => 'ITK-ISDN-Prot', + 'ascend_callback_delay' => 'Ascend-Callback-Delay', + 'session_error_code' => 'Session-Error-Code', + 'nomadix_endofsession' => 'Nomadix-EndofSession', + 'x_ascend_bacp_enable' => 'X-Ascend-BACP-Enable', + 'bg_trans_bpdu' => 'BG-Trans-BPDU', + 'bind_int_interface_namf' => 'Bind_Int_Interface_Name', + 'foundry_privilege_level' => 'Foundry-Privilege-Level', 'huntgroup_name' => 'Huntgroup-Name', - 'usr_pw_vpn_gateway' => 'USR-PW_VPN_Gateway', - 'ascend_x25_reverse_charg' => 'Ascend-X25-Reverse-Charging', - 'lac_real_port' => 'LAC_Real_Port', - 'ascend_dba_monitor' => 'Ascend-DBA-Monitor', - 'annex_user_server_locati' => 'Annex-User-Server-Location', - 'ascend_h323_fegw_address' => 'Ascend-H323-Fegw-Address', - 'acct_output_gigawords' => 'Acct-Output-Gigawords', - 'bind_l2tp_tunnel_name' => 'Bind_L2TP_Tunnel_Name', - 'x_ascend_token_idle' => 'X-Ascend-Token-Idle', - 'acc_apsm_oversubscribed' => 'Acc-Apsm-Oversubscribed', - 'ip_tos_field' => 'IP_TOS_Field', - 'ascend_dsl_cir_xmit_limi' => 'Ascend-Dsl-CIR-Xmit-Limit', - 'usr_number_of_link_naks' => 'USR-Number-of-Link-NAKs', - 'framed_address' => 'Framed-Address', - 'x_ascend_num_in_multilin' => 'X-Ascend-Num-In-Multilink', - 'hint' => 'Hint', - 'ascend_source_ip_check' => 'Ascend-Source-IP-Check', + 'x_ascend_ipx_alias' => 'X-Ascend-IPX-Alias', + 'tunnel_l2f_second_passwp' => 'Tunnel_L2F_Second_Password', + 'xedia_dns_server' => 'Xedia-DNS-Server', + 'usr_ipx_wan' => 'USR-IPX-WAN', + 'annex_addr_resolution_se' => 'Annex-Addr-Resolution-Servers', + 'acct_output_octets_65' => 'Acct_Output_Octets_64', + 'menu' => 'Menu', + 'erx_tunnel_nas_port_meth' => 'ERX-Tunnel-Nas-Port-Method', + 'aat_output_octets_diff' => 'AAT-Output-Octets-Diff', + 'x_ascend_fr_direct_dlci' => 'X-Ascend-FR-Direct-DLCI', + 'acct_status_type' => 'Acct-Status-Type', + 'ascend_port_redir_server' => 'Ascend-Port-Redir-Server', + 'telebit_port_name' => 'Telebit-Port-Name', + 'acc_dns_server_sec' => 'Acc-Dns-Server-Sec', + 'cvx_modem_remote_retrain' => 'CVX-Modem-Remote-Retrains', + 'ascend_minimum_channels' => 'Ascend-Minimum-Channels', + 'ascend_ipx_route' => 'Ascend-IPX-Route', + 'ascend_telnet_profile' => 'Ascend-Telnet-Profile', + 'usr_call_connect_in_gmt' => 'USR-Call-Connect-in-GMT', + 'usr_cusr_hat_script_rule' => 'USR-CUSR-hat-Script-Rules', + 'x_ascend_dba_monitor' => 'X-Ascend-DBA-Monitor', + 'response_packet_type' => 'Response-Packet-Type', + 'usr_event_id' => 'USR-Event-Id', + 'cvpn3000_ipsec_over_udp_' => 'CVPN3000-IPSec-Over-UDP-Port', + 'ascend_inc_channel_count' => 'Ascend-Inc-Channel-Count', + 'usr_send_script3' => 'USR-Send-Script3', + 'annex_pre_input_packets' => 'Annex-Pre-Input-Packets', + 'framed_callback_id' => 'Framed-Callback-Id', + 'xedia_client_access_netw' => 'Xedia-Client-Access-Network', 'arap_zone_access' => 'ARAP-Zone-Access', - 'x_ascend_fr_direct_profi' => 'X-Ascend-FR-Direct-Profile', - 'x_ascend_bridge_address' => 'X-Ascend-Bridge-Address', - 'usr_iwf_call_identifier' => 'USR-IWF-Call-Identifier', - 'ascend_home_network_name' => 'Ascend-Home-Network-Name', - 'ascend_require_auth' => 'Ascend-Require-Auth', - 'source_validation' => 'Source_Validation', + 'ascend_port_redir_portnu' => 'Ascend-Port-Redir-Portnum', + 'service_type' => 'Service-Type', + 'usr_nfas_id' => 'USR-NFAS-ID', + 'shiva_calling_number' => 'Shiva-Calling-Number', + 'ascend_user_acct_host' => 'Ascend-User-Acct-Host', + 'tunnel_session_auth_serv' => 'Tunnel-Session-Auth-Service-Grp', + 'juniper_deny_commands' => 'Juniper-Deny-Commands', + 'ascend_fr_link_mgt' => 'Ascend-FR-Link-Mgt', + 'nokia_imsi' => 'Nokia-IMSI', + 'quintum_h323_prompt_id' => 'Quintum-h323-prompt-id', + 'cvpn3000_require_individ' => 'CVPN3000-Require-Individual-User-Auth', + 'tunnel_retransmiu' => 'Tunnel_Retransmit', + 'source_validatioo' => 'Source_Validation', + 'sip_to' => 'Sip-To', 'ms_primary_nbns_server' => 'MS-Primary-NBNS-Server', - 'h323_setup_time' => 'h323-setup-time', - 'tunnel_remote_name' => 'Tunnel_Remote_Name', - 'ascend_maximum_channels' => 'Ascend-Maximum-Channels', - 'ascend_tunneling_protoco' => 'Ascend-Tunneling-Protocol', - 'arap_security_data' => 'ARAP-Security-Data', - 'ascend_ipx_peer_mode' => 'Ascend-IPX-Peer-Mode', - 'ascend_cir_timer' => 'Ascend-CIR-Timer', - 'ascend_ts_idle_limit' => 'Ascend-TS-Idle-Limit', + 'quintum_avpair' => 'Quintum-AVPair', + 'ascend_transit_number' => 'Ascend-Transit-Number', 'ascend_cache_refresh' => 'Ascend-Cache-Refresh', - 'usr_rmmie_status' => 'USR-RMMIE-Status', - 'annex_callback_portlist' => 'Annex-Callback-Portlist', - 'usr_port_tap' => 'USR-Port-Tap', - 'ascend_client_secondary_' => 'Ascend-Client-Secondary-DNS', - 'x_ascend_first_dest' => 'X-Ascend-First-Dest', - 'lac_port' => 'LAC_Port', + 'ascend_user_acct_type' => 'Ascend-User-Acct-Type', + 'usr_num_fax_pages_proces' => 'USR-Num-Fax-Pages-Processed', + 'usr_mic' => 'USR-MIC', + 'usr_failure_to_connect_r' => 'USR-Failure-to-Connect-Reason', + 'cisco_fax_auth_status' => 'Cisco-Fax-Auth-Status', + 'bind_dot1q_vlan_tag_ie' => 'Bind_Dot1q_Vlan_Tag_Id', + 'ms_chap2_success' => 'MS-CHAP2-Success', + 'erx_tunnel_virtual_route' => 'ERX-Tunnel-Virtual-Router', + 'cisco_idle_limit' => 'Cisco-Idle-Limit', + 'ascend_pw_lifetime' => 'Ascend-PW-Lifetime', + 'cvpn3000_access_hours' => 'CVPN3000-Access-Hours', + 'bintec_sapcirctable' => 'BinTec-sapCircTable', + 'usr_packet_bus_session' => 'USR-Packet-Bus-Session', + 'acct_input_packets_64' => 'Acct-Input-Packets-64', + 'ascend_x25_pad_x3_parame' => 'Ascend-X25-Pad-X3-Parameters', + 'usr_secondary_nbns_serve' => 'USR-Secondary_NBNS_Server', + 'ascend_modem_slotno' => 'Ascend-Modem-SlotNo', + 'digest_qop' => 'Digest-QOP', + 'usr_characters_received' => 'USR-Characters-Received', + 'rate_limit_ratf' => 'Rate_Limit_Rate', + 'ms_bap_usage' => 'MS-BAP-Usage', + 'cisco_data_filter' => 'Cisco-Data-Filter', + 'usr_simplified_v42bis_us' => 'USR-Simplified-V42bis-Usage', + 'h323_setup_time' => 'h323-setup-time', + 'annex_wan_number' => 'Annex-Wan-Number', + 'cvx_vpop_id' => 'CVX-VPOP-ID', + 'usr_pw_tunnel_authentica' => 'USR-PW_Tunnel_Authentication', + 'le_nat_outsource_inmap' => 'LE-NAT-Outsource-Inmap', + 'cvx_modem_begin_recv_lin' => 'CVX-Modem-Begin-Recv-Line-Lvl', + 'telebit_login_command' => 'Telebit-Login-Command', + 'cisco_command_code' => 'Cisco-Command-Code', + 'itk_ppp_auth_type' => 'ITK-PPP-Auth-Type', + 'bintec_qosiftable' => 'BinTec-qosIfTable', + 'x_ascend_mpp_idle_percen' => 'X-Ascend-MPP-Idle-Percent', + 'usr_sap_filter_in' => 'USR-SAP-Filter-In', + 'framed_appletalk_link' => 'Framed-AppleTalk-Link', + 'tunnel_domaio' => 'Tunnel_Domain', + 'usr_ipx' => 'USR-IPX', + 'nas_real_poru' => 'NAS_Real_Port', + 'shiva_connect_reason' => 'Shiva-Connect-Reason', + 'x_ascend_pre_output_octe' => 'X-Ascend-Pre-Output-Octets', + 'cisco_ppp_vj_slot_comp' => 'Cisco-PPP-VJ-Slot-Comp', + 'freeradius_proxied_to' => 'Freeradius-Proxied-To', + 'ascend_atm_vpi' => 'Ascend-ATM-Vpi', + 'acc_ml_mlx_admin_state' => 'Acc-ML-MLX-Admin-State', + 'cvx_modem_snr' => 'CVX-Modem-SNR', + 'usr_igmp_robustness' => 'USR-IGMP-Robustness', + 'annex_rate_reneg_req_rcv' => 'Annex-Rate-Reneg-Req-Rcvd', + 'add_prefix' => 'Add-Prefix', + 'x_ascend_call_by_call' => 'X-Ascend-Call-By-Call', + 'usr_last_callers_number_' => 'USR-Last-Callers-Number-ANI', + 'postauth_type' => 'PostAuth-Type', + 'pvc_circuit_paddinh' => 'PVC_Circuit_Padding', + 'usr_at_rtmp_input_filter' => 'USR-AT-RTMP-Input-Filter', + 'erx_igmp_enable' => 'ERX-Igmp-Enable', + 'bind_bypass_contexu' => 'Bind_Bypass_Context', + 'x_ascend_num_in_multilin' => 'X-Ascend-Num-In-Multilink', + 'usr_pw_packet' => 'USR-PW_Packet', + 'dialback_no' => 'Dialback-No', + 'ascend_ip_tos_precedence' => 'Ascend-IP-TOS-Precedence', + 'cvpn5000_vpn_password' => 'CVPN5000-VPN-Password', + 'annex_cli_filter' => 'Annex-CLI-Filter', + 'x_ascend_dial_number' => 'X-Ascend-Dial-Number', + 'usr_iwf_call_identifier' => 'USR-IWF-Call-Identifier', + 'ms_secondary_dns_server' => 'MS-Secondary-DNS-Server', + 'shiva_type_of_service' => 'Shiva-Type-Of-Service', + 'bind_ses_context' => 'Bind-Ses-Context', + 'acc_reason_code' => 'Acc-Reason-Code', + 'ms_chap_cpw_1' => 'MS-CHAP-CPW-1', + 'wispr_bandwidth_max_down' => 'WISPr-Bandwidth-Max-Down', + 'h323_call_type' => 'h323-call-type', + 'bind_bypass_bypast' => 'Bind_Bypass_Bypass', + 'usr_number_of_link_timeo' => 'USR-Number-of-Link-Timeouts', + 'ascend_fr_08_mode' => 'Ascend-FR-08-Mode', + 'usr_calling_party_number' => 'USR-Calling-Party-Number', + 'usr_reply_script2' => 'USR-Reply-Script2', + 'usr_security_login_limit' => 'USR-Security-Login-Limit', + 'cisco_link_compression' => 'Cisco-Link-Compression', + 'ascend_vrouter_name' => 'Ascend-VRouter-Name', + 'erx_ppp_auth_protocol' => 'ERX-PPP-Auth-Protocol', + 'x_ascend_call_block_dura' => 'X-Ascend-Call-Block-Duration', + 'usr_modem_setup_time' => 'USR-Modem-Setup-Time', + 'pppoe_urm' => 'PPPOE_URL', + 'cisco_ip_direct' => 'Cisco-IP-Direct', + 'x_ascend_temporary_rtes' => 'X-Ascend-Temporary-Rtes', + 'ascend_x25_pad_alias_3' => 'Ascend-X25-Pad-Alias-3', + 'annex_multilink_id' => 'Annex-Multilink-Id', + 'mcast_maxgroupt' => 'Mcast_MaxGroups', + 'configuration_token' => 'Configuration-Token', + 'ascend_h323_conference_i' => 'Ascend-H323-Conference-Id', + 'ascend_ipx_header_compre' => 'Ascend-IPX-Header-Compression', + 'stripped_user_name' => 'Stripped-User-Name', + 'usr_ipx_rip_output_filte' => 'USR-IPX-RIP-Output-Filter', + 'cisco_call_filter' => 'Cisco-Call-Filter', + 'nas_ipv6_address' => 'NAS-IPv6-Address', + 'termination_menu' => 'Termination-Menu', + 'ascend_shared_profile_en' => 'Ascend-Shared-Profile-Enable', + 'port_message' => 'Port-Message', + 'erx_ingress_policy_name' => 'ERX-Ingress-Policy-Name', + 'acc_service_profile' => 'Acc-Service-Profile', + 'ascend_bir_proxy' => 'Ascend-BIR-Proxy', + 'aat_ppp_address' => 'AAT-PPP-Address', + 'usr_mbi_ct_pri_card_span' => 'USR-Mbi_Ct_PRI_Card_Span_Line', + 'ascend_x25_nui_prompt' => 'Ascend-X25-Nui-Prompt', + 'itk_modem_pool_id' => 'ITK-Modem-Pool-Id', + 'usr_compression_reset_mo' => 'USR-Compression-Reset-Mode', + 'usr_unauthenticated_time' => 'USR-Unauthenticated-Time', + 'ascend_multicast_gleave_' => 'Ascend-Multicast-GLeave-Delay', 'acc_callback_cbcp_type' => 'Acc-Callback-CBCP-Type', - 'usr_call_reference_numbe' => 'USR-Call-Reference-Number', - 'mcast_receive' => 'Mcast_Receive', - 'x_ascend_link_compressio' => 'X-Ascend-Link-Compression', - 'ascend_inter_arrival_jit' => 'Ascend-Inter-Arrival-Jitter', - 'x_ascend_assign_ip_pool' => 'X-Ascend-Assign-IP-Pool', - 'usr_chassis_call_span' => 'USR-Chassis-Call-Span', - 'arap_password' => 'ARAP-Password', - 'usr_ip_default_route_opt' => 'USR-IP-Default-Route-Option', - 'ascend_endpoint_disc' => 'Ascend-Endpoint-Disc', - 'tunnel_dnis' => 'Tunnel_DNIS', - 'ms_acct_auth_type' => 'MS-Acct-Auth-Type', - 'ascend_ts_idle_mode' => 'Ascend-TS-Idle-Mode', - 'shasta_service_profile' => 'Shasta-Service-Profile', - 'usr_cdma_call_reference_' => 'USR-CDMA-Call-Reference-Number', - 'usr_at_zip_input_filter' => 'USR-AT-Zip-Input-Filter', - 'x_ascend_pw_warntime' => 'X-Ascend-PW-Warntime', - 'ascend_fr_direct_dlci' => 'Ascend-FR-Direct-DLCI', - 'usr_dte_ring_no_answer_l' => 'USR-DTE-Ring-No-Answer-Limit', - 'ascend_multicast_rate_li' => 'Ascend-Multicast-Rate-Limit', - 'usr_routing_protocol' => 'USR-Routing-Protocol', - 'pam_auth' => 'Pam-Auth', - 'client_dns_sec' => 'Client_DNS_Sec', - 'bg_trans_bpdu' => 'BG_Trans_BPDU', - 'police_rate' => 'Police_Rate', - 'calling_station_id' => 'Calling-Station-Id', - 'usr_called_party_number' => 'USR-Called-Party-Number', - 'shiva_network_protocols' => 'Shiva-Network-Protocols', - 'x_ascend_client_gateway' => 'X-Ascend-Client-Gateway', - 'acct_input_octets' => 'Acct-Input-Octets', - 'ascend_call_type' => 'Ascend-Call-Type', - 'annex_product_name' => 'Annex-Product-Name', - 'framed_compression' => 'Framed-Compression', - 'ascend_atm_direct' => 'Ascend-ATM-Direct', + 'medium_typf' => 'Medium_Type', + 'login_service' => 'Login-Service', + 'itk_username_prompt' => 'ITK-Username-Prompt', + 'ascend_dial_number' => 'Ascend-Dial-Number', + 'framed_ipv6_route' => 'Framed-IPv6-Route', 'x_ascend_remote_addr' => 'X-Ascend-Remote-Addr', - 'usr_tunneled_mlpp' => 'USR-Tunneled-MLPP', - 'le_ipsec_outsource_profi' => 'LE-IPSec-Outsource-Profile', - 'ascend_atm_vci' => 'Ascend-ATM-Vci', - 'usr_number_of_link_timeo' => 'USR-Number-of-Link-Timeouts', - 'usr_et_bridge_input_filt' => 'USR-ET-Bridge-Input-Filter', - 'x_ascend_fr_t391' => 'X-Ascend-FR-T391', - 'x_ascend_fr_t392' => 'X-Ascend-FR-T392', - 'h323_conf_id' => 'h323-conf-id', 'usr_call_end_date_time' => 'USR-Call-End-Date-Time', - 'ascend_fr_t391' => 'Ascend-FR-T391', - 'bg_aging_time' => 'BG_Aging_Time', - 'x_ascend_pre_output_pack' => 'X-Ascend-Pre-Output-Packets', - 'acc_dialout_auth_mode' => 'Acc-Dialout-Auth-Mode', - 'ascend_calling_subaddres' => 'Ascend-Calling-Subaddress', - 'ascend_fr_t392' => 'Ascend-FR-T392', - 'acct_link_count' => 'Acct-Link-Count', - 'usr_chassis_call_slot' => 'USR-Chassis-Call-Slot', - 'h323_credit_time' => 'h323-credit-time', - 'nas_port_id' => 'NAS-Port-Id', - 'x_ascend_call_filter' => 'X-Ascend-Call-Filter', - 'ascend_destination_nas_p' => 'Ascend-Destination-Nas-Port', - 'arap_features' => 'ARAP-Features', + 'bind_dot1q_slot' => 'Bind-Dot1q-Slot', + 'le_connect_detail' => 'LE-Connect-Detail', + 'annex_user_level' => 'Annex-User-Level', + 'tunnel_dnis' => 'Tunnel-DNIS', + 'assigned_ip_address' => 'Assigned-IP-Address', + 'acc_bridging_support' => 'Acc-Bridging-Support', + 'usr_channel' => 'USR-Channel', + 'arap_security_data' => 'ARAP-Security-Data', + 'bind_auth_service_grp' => 'Bind-Auth-Service-Grp', + 'cisco_abort_cause' => 'Cisco-Abort-Cause', + 'bg_span_dit' => 'BG_Span_Dis', + 'h323_voice_quality' => 'h323-voice-quality', + 'lac_real_port_typf' => 'LAC_Real_Port_Type', + 'usr_channel_connected_to' => 'USR-Channel-Connected-To', + 'ascend_client_assign_win' => 'Ascend-Client-Assign-WINS', + 'redcreek_tunneled_gatewa' => 'RedCreek-Tunneled-Gateway', + 'usr_number_of_fallbacks' => 'USR-Number-of-Fallbacks', + 'nokia_prepaid_ind' => 'Nokia-Prepaid-Ind', + 'nomadix_maxbytesup' => 'Nomadix-MaxBytesUp', + 'login_hosu' => 'Login-Host', + 'ascend_bir_enable' => 'Ascend-BIR-Enable', + 'usr_connect_time_limit' => 'USR-Connect-Time-Limit', + 'ascend_presession_time' => 'Ascend-PreSession-Time', + 'altiga_simultaneous_logi' => 'Altiga-Simultaneous-Logins-G/U', + 'cvpn3000_ipsec_default_d' => 'CVPN3000-IPSec-Default-Domain', + 'aat_atm_vci' => 'AAT-ATM-VCI', + 'extreme_netlogin_url_des' => 'Extreme-Netlogin-Url-Desc', + 'itk_auth_serv_ip' => 'ITK-Auth-Serv-IP', + 'erx_alternate_cli_vroute' => 'ERX-Alternate-Cli-Vrouter-Name', + 'framed_compression' => 'Framed-Compression', + 'ascend_svc_enabled' => 'Ascend-SVC-Enabled', + 'proxy_state' => 'Proxy-State', + 'aat_vrouter_name' => 'AAT-Vrouter-Name', + 'usr_rmmie_pwrlvl_farecho' => 'USR-RMMIE-PwrLvl-FarEcho-Canc', + 'nas_poru' => 'NAS-Port', + 'wispr_location_name' => 'WISPr-Location-Name', + 'digest_user_name' => 'Digest-User-Name', + 'ascend_modem_shelfno' => 'Ascend-Modem-ShelfNo', + 'shasta_user_privilege' => 'Shasta-User-Privilege', + 'bind_auth_protocol' => 'Bind-Auth-Protocol', + 'ascend_home_agent_passwo' => 'Ascend-Home-Agent-Password', + 'acct_interim_interval' => 'Acct-Interim-Interval', + 'ascend_history_weigh_typ' => 'Ascend-History-Weigh-Type', + 'ms_link_drop_time_limit' => 'MS-Link-Drop-Time-Limit', + 'hint' => 'Hint', + 'x_ascend_target_util' => 'X-Ascend-Target-Util', + 'acc_access_partition' => 'Acc-Access-Partition', + 'usr_power_supply_number' => 'USR-Power-Supply-Number', + 'x_ascend_multilink_id' => 'X-Ascend-Multilink-ID', + 'redcreek_tunneled_domain' => 'RedCreek-Tunneled-DomainName', + 'nomadix_bw_down' => 'Nomadix-Bw-Down', + 'acc_ipx_compression' => 'Acc-Ipx-Compression', + 'quintum_h323_setup_time' => 'Quintum-h323-setup-time', + 'cisco_target_util' => 'Cisco-Target-Util', + 'acc_ip_gateway_sec' => 'Acc-Ip-Gateway-Sec', + 'ascend_dsl_cir_xmit_limi' => 'Ascend-Dsl-CIR-Xmit-Limit', + 'ascend_ip_pool_definitio' => 'Ascend-IP-Pool-Definition', + 'bind_sub_user_at_contexu' => 'Bind_Sub_User_At_Context', + 'itk_dest_no' => 'ITK-Dest-No', + 'usr_connect_time' => 'USR-Connect-Time', + 'usr_call_start_date_time' => 'USR-Call-Start-Date-Time', + 'altiga_l2tp_encryption_g' => 'Altiga-L2TP-Encryption-G', + 'ascend_auth_delay' => 'Ascend-Auth-Delay', + 'ascend_x25_pad_x3_profil' => 'Ascend-X25-Pad-X3-Profile', + 'ascend_access_intercepta' => 'Ascend-Access-Intercept-Log', + 'ascend_home_agent_udp_po' => 'Ascend-Home-Agent-UDP-Port', + 'bind_tun_context' => 'Bind-Tun-Context', + 'dialback_name' => 'Dialback-Name', + 'h323_redirect_ip_address' => 'h323-redirect-ip-address', + 'annex_keypress_timeout' => 'Annex-Keypress-Timeout', + 'x_ascend_home_network_na' => 'X-Ascend-Home-Network-Name', + 'ascend_x25_pad_alias_1' => 'Ascend-X25-Pad-Alias-1', + 'ascend_call_attempt_limi' => 'Ascend-Call-Attempt-Limit', + 'quintum_h323_currency_ty' => 'Quintum-h323-currency-type', + 'ms_chap_response' => 'MS-CHAP-Response', + 'st_secondary_nbns_server' => 'ST-Secondary-NBNS-Server', 'x_ascend_history_weigh_t' => 'X-Ascend-History-Weigh-Type', - 'annex_host_restrict' => 'Annex-Host-Restrict', - 'usr_compression_reset_mo' => 'USR-Compression-Reset-Mode', - 'cisco_maximum_time' => 'Cisco-Maximum-Time', - 'tunnel_max_sessions' => 'Tunnel_Max_Sessions', - 'bind_ses_context' => 'Bind_Ses_Context', - 'x_ascend_ppp_vj_slot_com' => 'X-Ascend-PPP-VJ-Slot-Comp', - 'usr_mobile_numbytes_rxed' => 'USR-Mobile-NumBytes-Rxed', - 'usr_rmmie_last_update_ti' => 'USR-RMMIE-Last-Update-Time', - 'ascend_atm_loopback_cell' => 'Ascend-ATM-Loopback-Cell-Loss', - 'ascend_bir_proxy' => 'Ascend-BIR-Proxy', - 'acct_mcast_in_packets' => 'Acct_Mcast_In_Packets', - 'shiva_type_of_service' => 'Shiva-Type-Of-Service', - 'ascend_fr_dte_n392' => 'Ascend-FR-DTE-N392', - 'usr_at_call_input_filter' => 'USR-AT-Call-Input-Filter', + 'usr_max_channels' => 'USR-Max-Channels', 'ascend_fr_dte_n393' => 'Ascend-FR-DTE-N393', - 'x_ascend_backup' => 'X-Ascend-Backup', - 'char_noecho' => 'Char-Noecho', - 'usr_rmmie_last_update_ev' => 'USR-RMMIE-Last-Update-Event', - 'le_advice_of_charge' => 'LE-Advice-of-Charge', - 'ascend_calling_id_type_o' => 'Ascend-Calling-Id-Type-Of-Num', - 'ascend_pppoe_enable' => 'Ascend-PPPoE-Enable', - 'usr_sync_async_mode' => 'USR-Sync-Async-Mode', - 'state' => 'State', - 'x_ascend_user_acct_base' => 'X-Ascend-User-Acct-Base', - 'x_ascend_ipx_alias' => 'X-Ascend-IPX-Alias', - 'ascend_ip_tos' => 'Ascend-IP-TOS', - 'annex_secondary_dns_serv' => 'Annex-Secondary-DNS-Server', - 'tunnel_session_auth_ctx' => 'Tunnel_Session_Auth_Ctx', - 'usr_mbi_ct_pri_card_span' => 'USR-Mbi_Ct_PRI_Card_Span_Line', - 'usr_call_event_code' => 'USR-Call-Event-Code', - 'chap_password' => 'CHAP-Password', + 'ascend_pre_input_octets' => 'Ascend-Pre-Input-Octets', + 'erx_atm_mbs' => 'ERX-Atm-MBS', + 'cvpn3000_simultaneous_lo' => 'CVPN3000-Simultaneous-Logins', + 'juniper_allow_commands' => 'Juniper-Allow-Commands', + 'usr_line_reversals' => 'USR-Line-Reversals', + 'itk_users_default_pw' => 'ITK-Users-Default-Pw', + 'x_ascend_third_prompt' => 'X-Ascend-Third-Prompt', + 'cisco_fax_msg_id' => 'Cisco-Fax-Msg-Id', + 'x_ascend_pw_warntime' => 'X-Ascend-PW-Warntime', + 'ascend_data_filter' => 'Ascend-Data-Filter', + 'framed_address' => 'Framed-Address', + 'context_name' => 'Context-Name', + 'usr_send_script2' => 'USR-Send-Script2', + 'ms_arap_pw_change_reason' => 'MS-ARAP-PW-Change-Reason', + 'tunnel_session_auth_cty' => 'Tunnel_Session_Auth_Ctx', + 'acct_session_id' => 'Acct-Session-Id', + 'annex_port' => 'Annex-Port', + 'quintum_h323_call_origin' => 'Quintum-h323-call-origin', + 'erx_cli_initial_access_l' => 'ERX-Cli-Initial-Access-Level', + 'x_ascend_shared_profile_' => 'X-Ascend-Shared-Profile-Enable', + 'tunnel_cmd_timeouu' => 'Tunnel_Cmd_Timeout', + 'initial_modulation_type' => 'Initial-Modulation-Type', + 'ascend_h323_gatekeeper' => 'Ascend-H323-Gatekeeper', + 'x_ascend_fcp_parameter' => 'X-Ascend-FCP-Parameter', + 'multi_link_flag' => 'Multi-Link-Flag', + 'tunnel_type' => 'Tunnel-Type', + 'erx_output_gigapkts' => 'ERX-Output-Gigapkts', + 'ascend_idle_limit' => 'Ascend-Idle-Limit', + 'ns_user_group' => 'NS-User-Group', + 'password_retry' => 'Password-Retry', + 'h323_remote_address' => 'h323-remote-address', + 'erx_atm_service_category' => 'ERX-Atm-Service-Category', + 'acct_input_packets' => 'Acct-Input-Packets', + 'h323_disconnect_time' => 'h323-disconnect-time', + 'usr_syslog_tap' => 'USR-Syslog-Tap', + 'telebit_accounting_info' => 'Telebit-Accounting-Info', + 'ascend_billing_number' => 'Ascend-Billing-Number', + 'ascend_tunnel_vrouter_na' => 'Ascend-Tunnel-VRouter-Name', + 'ms_mppe_encryption_type' => 'MS-MPPE-Encryption-Type', + 'quintum_h323_credit_amou' => 'Quintum-h323-credit-amount', + 'acc_ace_token' => 'Acc-Ace-Token', + 'ascend_assign_ip_pool' => 'Ascend-Assign-IP-Pool', + 'annex_end_modulation' => 'Annex-End-Modulation', + 'usr_routing_protocol' => 'USR-Routing-Protocol', + 'cvx_assign_ip_pool' => 'CVX-Assign-IP-Pool', + 'usr_rad_location_type' => 'USR-Rad-Location-Type', + 'usr_rmmie_pwrlvl_noise_l' => 'USR-RMMIE-PwrLvl-Noise-Lvl', + 'usr_characters_sent' => 'USR-Characters-Sent', + 'usr_mp_edo_hiper' => 'USR-MP-EDO-HIPER', + 'ascend_x25_nui_password_' => 'Ascend-X25-Nui-Password-Prompt', + 'annex_host_restrict' => 'Annex-Host-Restrict', + 'user_service_type' => 'User-Service-Type', + 'acct_multi_session_id' => 'Acct-Multi-Session-Id', + 'ms_chap_cpw_2' => 'MS-CHAP-CPW-2', + 'x_ascend_secondary_home_' => 'X-Ascend-Secondary-Home-Agent', + 'x_ascend_dialout_allowed' => 'X-Ascend-Dialout-Allowed', + 'ascend_connect_progress' => 'Ascend-Connect-Progress', + 'x_ascend_ara_pw' => 'X-Ascend-Ara-PW', + 'cisco_fax_modem_time' => 'Cisco-Fax-Modem-Time', + 'sql_group' => 'Sql-Group', + 'annex_multicast_rate_lim' => 'Annex-Multicast-Rate-Limit', + 'cvpn3000_user_auth_servg' => 'CVPN3000-User-Auth-Server-Secret', + 'ns_mta_md5_password' => 'NS-MTA-MD5-Password', + 'annex_addr_resolution_pr' => 'Annex-Addr-Resolution-Protocol', + 'callback_number' => 'Callback-Number', + 'cvx_multilink_match_info' => 'CVX-Multilink-Match-Info', + 'tunnel_max_tunnelt' => 'Tunnel_Max_Tunnels', + 'tunnel_local_namf' => 'Tunnel_Local_Name', + 'quintum_h323_conf_id' => 'Quintum-h323-conf-id', + 'acct_output_packets_64' => 'Acct-Output-Packets-64', + 'annex_signal_to_noise_ra' => 'Annex-Signal-to-Noise-Ratio', + 'acct_output_packets_65' => 'Acct_Output_Packets_64', + 'x_ascend_user_acct_key' => 'X-Ascend-User-Acct-Key', + 'erx_dial_out_number' => 'ERX-Dial-Out-Number', + 'ascend_modem_portno' => 'Ascend-Modem-PortNo', + 'ascend_assign_ip_server' => 'Ascend-Assign-IP-Server', + 'ascend_fcp_parameter' => 'Ascend-FCP-Parameter', + 'usr_chassis_temp_thresho' => 'USR-Chassis-Temp-Threshold', + 'usr_mpip_tunnel_originat' => 'USR-MPIP-Tunnel-Originator', + 'tunnel_rate_limit_bursu' => 'Tunnel_Rate_Limit_Burst', + 'client_ip_address' => 'Client-IP-Address', 'le_nat_tcp_session_timeo' => 'LE-NAT-TCP-Session-Timeout', - 'usr_call_start_date_time' => 'USR-Call-Start-Date-Time', - 'usr_multicast_forwarding' => 'USR-Multicast-Forwarding', - 'client_id' => 'Client-Id', - 'sql_user_name' => 'SQL-User-Name', - 'x_ascend_billing_number' => 'X-Ascend-Billing-Number', - 'ms_secondary_nbns_server' => 'MS-Secondary-NBNS-Server', - 'cisco_num_in_multilink' => 'Cisco-Num-In-Multilink', + 'quintum_h323_redirect_ip' => 'Quintum-h323-redirect-ip-address', + 'ms_acct_eap_type' => 'MS-Acct-EAP-Type', + 'usr_rmmie_x2_status' => 'USR-RMMIE-x2-Status', + 'x_ascend_user_acct_type' => 'X-Ascend-User-Acct-Type', + 'shiva_customer_id' => 'Shiva-Customer-Id', + 'pvc_encapsulation_typf' => 'PVC_Encapsulation_Type', + 'st_acct_vc_connection_id' => 'ST-Acct-VC-Connection-Id', + 'lac_real_port' => 'LAC-Real-Port', + 'h323_connect_time' => 'h323-connect-time', + 'usr_vpn_gw_location_id' => 'USR-VPN-GW-Location-Id', + 'old_password' => 'Old-Password', + 'x_ascend_if_netmask' => 'X-Ascend-IF-Netmask', + 'add_suffix' => 'Add-Suffix', + 'lac_port_typf' => 'LAC_Port_Type', + 'acc_ip_pool_name' => 'Acc-Ip-Pool-Name', + 'usr_terminal_type' => 'USR-Terminal-Type', + 'usr_spoofing' => 'USR-Spoofing', + 'erx_tunnel_password' => 'ERX-Tunnel-Password', + 'ascend_inter_arrival_jit' => 'Ascend-Inter-Arrival-Jitter', + 'ascend_call_block_durati' => 'Ascend-Call-Block-Duration', + 'itk_channel_binding' => 'ITK-Channel-Binding', + 'usr_server_time' => 'USR-Server-Time', + 'ascend_assign_ip_client' => 'Ascend-Assign-IP-Client', + 'erx_pppoe_max_sessions' => 'ERX-Pppoe-Max-Sessions', + 'cvx_multilink_group_numb' => 'CVX-Multilink-Group-Number', 'x_ascend_client_assign_d' => 'X-Ascend-Client-Assign-DNS', - 'x_ascend_user_acct_port' => 'X-Ascend-User-Acct-Port', - 'usr_local_ip_address' => 'USR-Local-IP-Address', - 'x_ascend_ip_pool_definit' => 'X-Ascend-IP-Pool-Definition', - 'ascend_metric' => 'Ascend-Metric', - 'x_ascend_bacp_enable' => 'X-Ascend-BACP-Enable', - 'x_ascend_user_acct_time' => 'X-Ascend-User-Acct-Time', - 'x_ascend_mpp_idle_percen' => 'X-Ascend-MPP-Idle-Percent', + 'erx_pppoe_url' => 'ERX-Pppoe-Url', + 'police_ratf' => 'Police_Rate', + 'ascend_data_svc' => 'Ascend-Data-Svc', 'annex_authen_servers' => 'Annex-Authen-Servers', - 'x_ascend_data_filter' => 'X-Ascend-Data-Filter', - 'ascend_idle_limit' => 'Ascend-Idle-Limit', - 'ldap_userdn' => 'Ldap-UserDn', - 'x_ascend_target_util' => 'X-Ascend-Target-Util', - 'shiva_connect_reason' => 'Shiva-Connect-Reason', - 'usr_ds0' => 'USR-DS0', - 'annex_re_chap_timeout' => 'Annex-Re-CHAP-Timeout', - 'shasta_vpn_name' => 'Shasta-VPN-Name', - 'acct_tunnel_connection_i' => 'Acct-Tunnel-Connection-Id', - 'h323_prompt_id' => 'h323-prompt-id', - 'x_ascend_ipx_peer_mode' => 'X-Ascend-IPX-Peer-Mode', - 'ascend_numbering_plan_id' => 'Ascend-Numbering-Plan-ID', - 'x_ascend_ts_idle_limit' => 'X-Ascend-TS-Idle-Limit', - 'ascend_atm_fault_managem' => 'Ascend-ATM-Fault-Management', - 'annex_primary_nbns_serve' => 'Annex-Primary-NBNS-Server', - 'lac_port_type' => 'LAC_Port_Type', - 'usr_initial_rx_link_data' => 'USR-Initial-Rx-Link-Data-Rate', - 'usr_interface_index' => 'USR-Interface-Index', + 'nomadix_bw_up' => 'Nomadix-Bw-Up', + 'cvx_modem_data_compressi' => 'CVX-Modem-Data-Compression', + 'shiva_link_speed' => 'Shiva-Link-Speed', + 'usr_reply_script6' => 'USR-Reply-Script6', 'usr_expansion_algorithm' => 'USR-Expansion-Algorithm', - 'ascend_tunnel_vrouter_na' => 'Ascend-Tunnel-VRouter-Name', - 'usr_pw_vpn_neighbor' => 'USR-PW_VPN_Neighbor', - 'bind_type' => 'Bind_Type', - 'acc_ccp_option' => 'Acc-Ccp-Option', - 'ascend_route_appletalk' => 'Ascend-Route-Appletalk', + 'cabletron_protocol_calla' => 'Cabletron-Protocol-Callable', + 'cisco_data_rate' => 'Cisco-Data-Rate', + 'usr_primary_dns_server' => 'USR-Primary_DNS_Server', + 'juniper_deny_configurati' => 'Juniper-Deny-Configuration', + 'ascend_target_util' => 'Ascend-Target-Util', + 'digest_method' => 'Digest-Method', + 'altiga_ipsec_split_tunne' => 'Altiga-IPSec-Split-Tunnel-List-G', 'erx_alternate_cli_access' => 'ERX-Alternate-Cli-Access-Level', - 'usr_at_rtmp_output_filte' => 'USR-AT-RTMP-Output-Filter', - 'erx_atm_mbs' => 'ERX-Atm-MBS', + 'x_ascend_event_type' => 'X-Ascend-Event-Type', + 'usr_q931_call_reference_' => 'USR-Q931-Call-Reference-Value', + 'usr_mp_mrru' => 'USR-MP-MRRU', + 'cvx_ipsvc_mask' => 'CVX-IPSVC-Mask', + 'bind_bypass_context' => 'Bind-Bypass-Context', + 'usr_rmmie_last_update_ev' => 'USR-RMMIE-Last-Update-Event', + 'no_such_attribute' => 'No-Such-Attribute', + 'acct_mcast_out_packets' => 'Acct-Mcast-Out-Packets', + 'tunnel_medium_type' => 'Tunnel-Medium-Type', + 'quintum_h323_remote_addr' => 'Quintum-h323-remote-address', + 'acc_callback_delay' => 'Acc-Callback-Delay', + 'acct_input_octets_64' => 'Acct-Input-Octets-64', + 'ascend_base_channel_coun' => 'Ascend-Base-Channel-Count', + 'ascend_atm_connect_vci' => 'Ascend-ATM-Connect-Vci', + 'erx_primary_dns' => 'ERX-Primary-Dns', + 'altiga_ipsec_over_nat_g' => 'Altiga-IPSec-Over-NAT-G', + 'cvx_multicast_rate_limit' => 'CVX-Multicast-Rate-Limit', + 'ascend_xmit_rate' => 'Ascend-Xmit-Rate', + 'ms_new_arap_password' => 'MS-New-ARAP-Password', + 'usr_call_error_code' => 'USR-Call-Error-Code', + 'acct_output_octets' => 'Acct-Output-Octets', + 'ascend_client_primary_wi' => 'Ascend-Client-Primary-WINS', + 'cvpn3000_primary_wins' => 'CVPN3000-Primary-WINS', + 'bintec_ipextrttable' => 'BinTec-ipExtRtTable', + 'cisco_fax_mdn_flag' => 'Cisco-Fax-Mdn-Flag', + 'ascend_destination_nas_p' => 'Ascend-Destination-Nas-Port', + 'ascend_num_in_multilink' => 'Ascend-Num-In-Multilink', + 'digest_attributes' => 'Digest-Attributes', + 'cvpn3000_ipsec_tunnel_ty' => 'CVPN3000-IPSec-Tunnel-Type', + 'x_ascend_number_sessions' => 'X-Ascend-Number-Sessions', + 'usr_ip_rip_output_filter' => 'USR-IP-RIP-Output-Filter', + 'tunnel_police_bursu' => 'Tunnel_Police_Burst', + 'redcreek_tunneled_wins_s' => 'RedCreek-Tunneled-WINS-Server1', + 'usr_blocks_sent' => 'USR-Blocks-Sent', + 'erx_cli_allow_all_vr_acc' => 'ERX-Cli-Allow-All-VR-Access', + 'tunnel_police_ratf' => 'Tunnel_Police_Rate', + 'usr_ids0_call_type' => 'USR-IDS0-Call-Type', + 'acc_ccp_option' => 'Acc-Ccp-Option', + 'ascend_client_gateway' => 'Ascend-Client-Gateway', + 'cvx_maximum_channels' => 'CVX-Maximum-Channels', + 'bg_aging_timf' => 'BG_Aging_Time', + 'annex_secondary_dns_serv' => 'Annex-Secondary-DNS-Server', + 'le_ipsec_passive_profile' => 'LE-IPSec-Passive-Profile', + 'usr_chassis_call_span' => 'USR-Chassis-Call-Span', + 'aat_client_primary_wins_' => 'AAT-Client-Primary-WINS-NBNS', + 'h323_currency' => 'h323-currency', + 'password' => 'Password', + 'le_nat_log_options' => 'LE-NAT-Log-Options', + 'usr_fallback_limit' => 'USR-Fallback-Limit', + 'x_ascend_ppp_address' => 'X-Ascend-PPP-Address', + 'suffix' => 'Suffix', + 'usr_multicast_receive' => 'USR-Multicast-Receive', + 'client_dns_sec' => 'Client-DNS-Sec', + 'annex_product_name' => 'Annex-Product-Name', + 'cisco_pw_lifetime' => 'Cisco-PW-Lifetime', + 'x_ascend_fr_dce_n393' => 'X-Ascend-FR-DCE-N393', + 'x_ascend_ts_idle_limit' => 'X-Ascend-TS-Idle-Limit', + 'mcast_send' => 'Mcast-Send', + 'x_ascend_primary_home_ag' => 'X-Ascend-Primary-Home-Agent', + 'tunnel_max_sessiont' => 'Tunnel_Max_Sessions', + 'pppoe_motm' => 'PPPOE-MOTM', + 'usr_pw_usr_ifilter_ipx' => 'USR-PW_USR_IFilter_IPX', + 'ms_ras_version' => 'MS-RAS-Version', + 'ascend_source_ip_check' => 'Ascend-Source-IP-Check', + 'bintec_ospfiftable' => 'BinTec-ospfIfTable', + 'acc_ml_call_threshold' => 'Acc-ML-Call-Threshold', + 'x_ascend_modem_slotno' => 'X-Ascend-Modem-SlotNo', + 'ascend_menu_item' => 'Ascend-Menu-Item', + 'callback_id' => 'Callback-Id', + 'framed_ipx_network' => 'Framed-IPX-Network', + 'altiga_pptp_encryption_g' => 'Altiga-PPTP-Encryption-G', + 'ascend_x25_reverse_charg' => 'Ascend-X25-Reverse-Charging', + 'ascend_user_acct_key' => 'Ascend-User-Acct-Key', + 'x_ascend_pw_lifetime' => 'X-Ascend-PW-Lifetime', + 'user_name_is_star' => 'User-Name-Is-Star', + 'nomadix_url_redirection' => 'Nomadix-URL-Redirection', + 'framed_pool' => 'Framed-Pool', + 'x_ascend_authen_alias' => 'X-Ascend-Authen-Alias', + 'cisco_fax_dsn_address' => 'Cisco-Fax-Dsn-Address', + 'ms_primary_dns_server' => 'MS-Primary-DNS-Server', + 'acc_dialout_auth_usernam' => 'Acc-Dialout-Auth-Username', + 'realm' => 'Realm', + 'arap_features' => 'ARAP-Features', + 'bind_auth_protocom' => 'Bind_Auth_Protocol', + 'acc_connect_tx_speed' => 'Acc-Connect-Tx-Speed', + 'usr_chassis_temperature' => 'USR-Chassis-Temperature', + 'altiga_ipsec_mode_config' => 'Altiga-IPSec-Mode-Config-G', + 'ascend_home_agent_ip_add' => 'Ascend-Home-Agent-IP-Addr', + 'x_ascend_xmit_rate' => 'X-Ascend-Xmit-Rate', + 'cvpn3000_secondary_dns' => 'CVPN3000-Secondary-DNS', + 'x_ascend_send_passwd' => 'X-Ascend-Send-Passwd', + 'bind_int_contexu' => 'Bind_Int_Context', + 'cisco_fax_account_id_ori' => 'Cisco-Fax-Account-Id-Origin', + 'le_modem_info' => 'LE-Modem-Info', + 'ascend_ipx_peer_mode' => 'Ascend-IPX-Peer-Mode', + 'juniper_local_user_name' => 'Juniper-Local-User-Name', + 'tunnel_rate_limit_rate' => 'Tunnel-Rate-Limit-Rate', + 'quintum_h323_credit_time' => 'Quintum-h323-credit-time', + 'acc_modem_modulation_typ' => 'Acc-Modem-Modulation-Type', + 'x_ascend_seconds_of_hist' => 'X-Ascend-Seconds-Of-History', + 'ascend_dhcp_pool_number' => 'Ascend-DHCP-Pool-Number', + 'redcreek_tunneled_ip_net' => 'RedCreek-Tunneled-IP-Netmask', + 'x_ascend_callback' => 'X-Ascend-Callback', + 'usr_iwf_ip_address' => 'USR-IWF-IP-Address', + 'aat_input_octets_diff' => 'AAT-Input-Octets-Diff', + 'nas_port_id' => 'NAS-Port-Id', + 'le_advice_of_charge' => 'LE-Advice-of-Charge', + 'x_ascend_dhcp_pool_numbe' => 'X-Ascend-DHCP-Pool-Number', + 'ascend_add_seconds' => 'Ascend-Add-Seconds', + 'annex_transmit_speed' => 'Annex-Transmit-Speed', + 'usr_port_tap' => 'USR-Port-Tap', + 'usr_at_call_input_filter' => 'USR-AT-Call-Input-Filter', + 'framed_ipv6_pool' => 'Framed-IPv6-Pool', + 'ascend_qos_downstream' => 'Ascend-QOS-Downstream', + 'lac_port' => 'LAC-Port', + 'tunnel_assignment_id' => 'Tunnel-Assignment-Id', + 'acct_mcast_out_octett' => 'Acct_Mcast_Out_Octets', + 'ascend_bi_directional_au' => 'Ascend-Bi-Directional-Auth', + 'fall_through' => 'Fall-Through', + 'cvpn3000_ipsec_ip_compre' => 'CVPN3000-IPSec-IP-Compression', + 'cisco_disconnect_cause' => 'Cisco-Disconnect-Cause', + 'usr_rad_multicast_routiq' => 'USR-Rad-Multicast-Routing-Bound', + 'altiga_tunneling_protoco' => 'Altiga-Tunneling-Protocols-G/U', + 'itk_tunnel_prot' => 'ITK-Tunnel-Prot', + 'client_dns_sed' => 'Client_DNS_Sec', + 'framed_ip_netmask' => 'Framed-IP-Netmask', + 'usr_call_reference_numbe' => 'USR-Call-Reference-Number', + 'ascend_egress_enabled' => 'Ascend-Egress-Enabled', + 'ascend_dsl_rate_mode' => 'Ascend-Dsl-Rate-Mode', + 'usr_pw_usr_ofilter_sap' => 'USR-PW_USR_OFilter_SAP', + 'bintec_iproutetable' => 'BinTec-ipRouteTable', + 'acct_terminate_cause' => 'Acct-Terminate-Cause', + 'x_ascend_fr_dte_n393' => 'X-Ascend-FR-DTE-N393', + 'ascend_ppp_address' => 'Ascend-PPP-Address', + 'erx_maximum_bps' => 'ERX-Maximum-BPS', + 'caller_id' => 'Caller-ID', + 'bintec_ipfiltertable' => 'BinTec-ipFilterTable', + 'x_ascend_base_channel_co' => 'X-Ascend-Base-Channel-Count', + 'bind_int_interface_name' => 'Bind-Int-Interface-Name', + 'usr_modem_group' => 'USR-Modem-Group', + 'cisco_maximum_channels' => 'Cisco-Maximum-Channels', + 'erx_ppp_username' => 'ERX-PPP-Username', + 'ascend_link_compression' => 'Ascend-Link-Compression', + 'annex_retransmitted_pack' => 'Annex-Retransmitted-Packets', + 'usr_retrains_granted' => 'USR-Retrains-Granted', + 'ascend_dropped_packets' => 'Ascend-Dropped-Packets', + 'erx_bearer_type' => 'ERX-Bearer-Type', + 'usr_pw_usr_ofilter_ip' => 'USR-PW_USR_OFilter_IP', + 'quintum_nas_port' => 'Quintum-NAS-Port', + 'x_ascend_pre_output_pack' => 'X-Ascend-Pre-Output-Packets', + 'usr_cdma_call_reference_' => 'USR-CDMA-Call-Reference-Number', + 'tunnel_function' => 'Tunnel-Function', + 'annex_tunnel_authen_mode' => 'Annex-Tunnel-Authen-Mode', + 'usr_mp_edo' => 'USR-MP-EDO', + 'le_nat_outmap' => 'LE-NAT-Outmap', + 'cvpn3000_primary_dns' => 'CVPN3000-Primary-DNS', + 'usr_modulation_type' => 'USR-Modulation-Type', + 'ascend_calling_id_screen' => 'Ascend-Calling-Id-Screening', + 'ascend_maximum_time' => 'Ascend-Maximum-Time', + 'user_password' => 'User-Password', + 'annex_callback_portlist' => 'Annex-Callback-Portlist', + 'cvpn3000_ipsec_split_tun' => 'CVPN3000-IPSec-Split-Tunnel-List', + 'annex_pre_output_packets' => 'Annex-Pre-Output-Packets', 'usr_at_call_output_filte' => 'USR-AT-Call-Output-Filter', - 'ms_old_arap_password' => 'MS-Old-ARAP-Password', 'x_ascend_client_primary_' => 'X-Ascend-Client-Primary-DNS', - 'x_ascend_host_info' => 'X-Ascend-Host-Info', - 'bind_auth_protocol' => 'Bind_Auth_Protocol', - 'cisco_link_compression' => 'Cisco-Link-Compression', - 'annex_syslog_tap' => 'Annex-Syslog-Tap', - 'tunnel_window' => 'Tunnel_Window', - 'usr_gateway_ip_address' => 'USR-Gateway-IP-Address', - 'ascend_redirect_number' => 'Ascend-Redirect-Number', - 'x_ascend_secondary_home_' => 'X-Ascend-Secondary-Home-Agent', - 'usr_pw_index' => 'USR-PW_Index', - 'le_multicast_client' => 'LE-Multicast-Client', - 'annex_modem_disc_reason' => 'Annex-Modem-Disc-Reason', - 'annex_primary_dns_server' => 'Annex-Primary-DNS-Server', - 'erx_secondary_wins' => 'ERX-Secondary-Wins', - 'fall_through' => 'Fall-Through', - 'acct_mcast_out_packets' => 'Acct_Mcast_Out_Packets', - 'x_ascend_transit_number' => 'X-Ascend-Transit-Number', - 'usr_unauthenticated_time' => 'USR-Unauthenticated-Time', - 'le_ipsec_active_profile' => 'LE-IPSec-Active-Profile', - 'ascend_ip_pool_chaining' => 'Ascend-IP-Pool-Chaining', - 'usr_syslog_tap' => 'USR-Syslog-Tap', - 'ascend_multicast_client' => 'Ascend-Multicast-Client', - 'usr_device_connected_to' => 'USR-Device-Connected-To', - 'tunnel_l2f_second_passwo' => 'Tunnel_L2F_Second_Password', - 'add_prefix' => 'Add-Prefix', - 'tunnel_cmd_timeout' => 'Tunnel_Cmd_Timeout', + 'tunnel_server_endpoint' => 'Tunnel-Server-Endpoint', 'x_ascend_remove_seconds' => 'X-Ascend-Remove-Seconds', - 'acct_mcast_in_octets' => 'Acct_Mcast_In_Octets', - 'ascend_appletalk_route' => 'Ascend-Appletalk-Route', - 'ascend_fcp_parameter' => 'Ascend-FCP-Parameter', - 'acc_ip_compression' => 'Acc-Ip-Compression', - 'usr_modem_training_time' => 'USR-Modem-Training-Time', - 'usr_primary_dns_server' => 'USR-Primary_DNS_Server', - 'erx_egress_policy_name' => 'ERX-Egress-Policy-Name', - 'x_ascend_base_channel_co' => 'X-Ascend-Base-Channel-Count', - 'x_ascend_pre_input_packe' => 'X-Ascend-Pre-Input-Packets', - 'password_retry' => 'Password-Retry', + 'cvpn3000_user_auth_serve' => 'CVPN3000-User-Auth-Server-Name', + 'arap_password' => 'ARAP-Password', + 'x_ascend_assign_ip_serve' => 'X-Ascend-Assign-IP-Server', + 'cisco_fax_pages' => 'Cisco-Fax-Pages', + 'ms_chap_mppe_keys' => 'MS-CHAP-MPPE-Keys', 'ascend_source_auth' => 'Ascend-Source-Auth', - 'cisco_pw_lifetime' => 'Cisco-PW-Lifetime', - 'acc_dns_server_pri' => 'Acc-Dns-Server-Pri', - 'ascend_netware_timeout' => 'Ascend-Netware-timeout', - 'ascend_ppp_async_map' => 'Ascend-PPP-Async-Map', - 'usr_rad_multicast_routin' => 'USR-Rad-Multicast-Routing-Ttl', - 'x_ascend_modem_slotno' => 'X-Ascend-Modem-SlotNo', - 'x_ascend_ip_direct' => 'X-Ascend-IP-Direct', - 'simultaneous_use' => 'Simultaneous-Use', - 'erx_virtual_router_name' => 'ERX-Virtual-Router-Name', - 'ascend_bridge_non_pppoe' => 'Ascend-Bridge-Non-PPPoE', - 'ascend_fr_08_mode' => 'Ascend-FR-08-Mode', - 'h323_call_type' => 'h323-call-type', - 'tunnel_context' => 'Tunnel_Context', - 'usr_transmit_acc_map' => 'USR-Transmit-Acc-Map', - 'usr_ipx_wan' => 'USR-IPX-WAN', - 'usr_ip_call_input_filter' => 'USR-IP-Call-Input-Filter', - 'usr_call_connect_in_gmt' => 'USR-Call-Connect-in-GMT', - 'acct_multi_session_id' => 'Acct-Multi-Session-Id', - 'usr_reply_script1' => 'USR-Reply-Script1', - 'cisco_ppp_vj_slot_comp' => 'Cisco-PPP-VJ-Slot-Comp', - 'usr_reply_script2' => 'USR-Reply-Script2', - 'usr_reply_script3' => 'USR-Reply-Script3', - 'usr_reply_script4' => 'USR-Reply-Script4', - 'usr_reply_script5' => 'USR-Reply-Script5', - 'usr_reply_script6' => 'USR-Reply-Script6', - 'user_category' => 'User-Category', - 'mcast_send' => 'Mcast_Send', + 'group' => 'Group', + 'usr_send_script6' => 'USR-Send-Script6', + 'le_nat_inmap' => 'LE-NAT-Inmap', + 'chap_password' => 'CHAP-Password', + 'annex_receive_speed' => 'Annex-Receive-Speed', + 'usr_mobileip_home_agent_' => 'USR-MobileIP-Home-Agent-Address', + 'bind_l2tp_flow_control' => 'Bind-L2TP-Flow-Control', + 'smb_account_ctrl' => 'SMB-Account-CTRL', + 'ascend_ip_pool_chaining' => 'Ascend-IP-Pool-Chaining', + 'le_admin_group' => 'LE-Admin-Group', + 'tunnel_connection_id' => 'Tunnel-Connection-Id', + 'tunnel_windox' => 'Tunnel_Window', + 'nas_identifier' => 'NAS-Identifier', + 'dhcp_max_leaset' => 'DHCP_Max_Leases', + 'digest_nonce_count' => 'Digest-Nonce-Count', + 'nas_real_port' => 'NAS-Real-Port', + 'ms_old_arap_password' => 'MS-Old-ARAP-Password', + 'usr_pw_index' => 'USR-PW_Index', + 'erx_primary_wins' => 'ERX-Primary-Wins', + 'ascend_appletalk_peer_mo' => 'Ascend-Appletalk-Peer-Mode', + 'le_ipsec_log_options' => 'LE-IPSec-Log-Options', + 'x_ascend_maximum_channel' => 'X-Ascend-Maximum-Channels', + 'cvx_ipsvc_aznlvl' => 'CVX-IPSVC-AZNLVL', + 'x_ascend_client_secondar' => 'X-Ascend-Client-Secondary-DNS', + 'annex_re_chap_timeout' => 'Annex-Re-CHAP-Timeout', + 'aat_ip_pool_definition' => 'AAT-IP-Pool-Definition', + 'client_dns_pri' => 'Client-DNS-Pri', + 'cisco_service_info' => 'Cisco-Service-Info', + 'usr_primary_nbns_server' => 'USR-Primary_NBNS_Server', + 'aat_atm_direct' => 'AAT-ATM-Direct', + 'bind_ses_contexu' => 'Bind_Ses_Context', + 'sip_translated_request_u' => 'Sip-Translated-Request-URI', + 'acc_acct_on_off_reason' => 'Acc-Acct-On-Off-Reason', + 'le_multicast_client' => 'LE-Multicast-Client', + 'bind_sub_passwore' => 'Bind_Sub_Password', + 'cvpn3000_cisco_ip_phone_' => 'CVPN3000-Cisco-IP-Phone-Bypass', + 'ascend_send_passwd' => 'Ascend-Send-Passwd', + 'tunnel_remote_namf' => 'Tunnel_Remote_Name', + 'cvx_disconnect_cause' => 'CVX-Disconnect-Cause', + 'itk_auth_serv_prot' => 'ITK-Auth-Serv-Prot', + 'tunnel_context' => 'Tunnel-Context', + 'digest_uri' => 'Digest-URI', + 'usr_channel_decrement' => 'USR-Channel-Decrement', + 'acc_nbns_server_sec' => 'Acc-Nbns-Server-Sec', + 'ms_chap_challenge' => 'MS-CHAP-Challenge', + 'cisco_assign_ip_pool' => 'Cisco-Assign-IP-Pool', + 'ascend_cbcp_mode' => 'Ascend-CBCP-Mode', + 'ascend_x25_rpoa' => 'Ascend-X25-Rpoa', + 'usr_dtr_false_timeout' => 'USR-DTR-False-Timeout', + 'acct_dyn_ac_enu' => 'Acct_Dyn_Ac_Ent', + 'usr_physical_state' => 'USR-Physical-State', + 'x_ascend_ppp_vj_slot_com' => 'X-Ascend-PPP-VJ-Slot-Comp', + 'x_ascend_link_compressio' => 'X-Ascend-Link-Compression', + 'ascend_fr_t391' => 'Ascend-FR-T391', + 'bind_dot1q_port' => 'Bind-Dot1q-Port', + 'ns_secondary_dns' => 'NS-Secondary-DNS', + 'altiga_ipsec_tunnel_type' => 'Altiga-IPSec-Tunnel-Type-G', + 'lac_port_type' => 'LAC-Port-Type', + 'bg_aging_time' => 'BG-Aging-Time', + 'erx_atm_scr' => 'ERX-Atm-SCR', + 'x_ascend_pre_input_octet' => 'X-Ascend-Pre-Input-Octets', + 'cisco_fax_connect_speed' => 'Cisco-Fax-Connect-Speed', + 'x_ascend_menu_item' => 'X-Ascend-Menu-Item', + 'quintum_h323_voice_quali' => 'Quintum-h323-voice-quality', + 'ascend_x25_pad_banner' => 'Ascend-X25-Pad-Banner', + 'module_failure_message' => 'Module-Failure-Message', + 'h323_gw_id' => 'h323-gw-id', + 'h323_preferred_lang' => 'h323-preferred-lang', + 'usr_min_compression_size' => 'USR-Min-Compression-Size', + 'usr_compression_type' => 'USR-Compression-Type', + 'bintec_ipxstaticroutetab' => 'BinTec-ipxStaticRouteTable', + 'ascend_dialout_allowed' => 'Ascend-Dialout-Allowed', + 'annex_local_username' => 'Annex-Local-Username', + 'cisco_pre_input_packets' => 'Cisco-Pre-Input-Packets', + 'shiva_function' => 'Shiva-Function', 'ascend_send_secret' => 'Ascend-Send-Secret', - 'usr_tunnel_switch_endpoi' => 'USR-Tunnel-Switch-Endpoint', - 'tunnel_retransmit' => 'Tunnel_Retransmit', - 'add_port_to_ip_address' => 'Add-Port-To-IP-Address', - 'ascend_ipx_node_addr' => 'Ascend-IPX-Node-Addr', - 'x_ascend_netware_timeout' => 'X-Ascend-Netware-timeout', - 'erx_sa_validate' => 'ERX-Sa-Validate', - 'le_ipsec_passive_profile' => 'LE-IPSec-Passive-Profile', + 'usr_number_of_blers' => 'USR-Number-of-Blers', + 'usr_dte_data_idle_timout' => 'USR-DTE-Data-Idle-Timout', + 'usr_card_type' => 'USR-Card-Type', + 'x_ascend_connect_progres' => 'X-Ascend-Connect-Progress', + 'x_ascend_group' => 'X-Ascend-Group', + 'ascend_token_idle' => 'Ascend-Token-Idle', + 'erx_qos_profile_interfac' => 'ERX-Qos-Profile-Interface-Type', + 'ascend_private_route_tab' => 'Ascend-Private-Route-Table-ID', + 'nt_password' => 'NT-Password', + 'acct_mcast_in_packets' => 'Acct-Mcast-In-Packets', + 'x_ascend_multicast_clien' => 'X-Ascend-Multicast-Client', + 'usr_supports_tags' => 'USR-Supports-Tags', + 'cvpn3000_authd_user_idle' => 'CVPN3000-Authd-User-Idle-Timeout', + 'ascend_number_sessions' => 'Ascend-Number-Sessions', + 'x_ascend_add_seconds' => 'X-Ascend-Add-Seconds', + 'usr_number_of_upshifts' => 'USR-Number-of-Upshifts', + 'proxy_to_realm' => 'Proxy-To-Realm', + 'aat_client_secondary_win' => 'AAT-Client-Secondary-WINS-NBNS', + 'aat_ip_tos_precedence' => 'AAT-IP-TOS-Precedence', + 'acc_callback_num_valid' => 'Acc-Callback-Num-Valid', + 'nokia_ggsn_ip_address' => 'Nokia-GGSN-IP-Address', + 'acc_access_community' => 'Acc-Access-Community', + 'ascend_multicast_rate_li' => 'Ascend-Multicast-Rate-Limit', + 'usr_default_dte_data_rat' => 'USR-Default-DTE-Data-Rate', + 'usr_rmmie_pwrlvl_nearech' => 'USR-RMMIE-PwrLvl-NearEcho-Canc', + 'usr_send_name' => 'USR-Send-Name', 'usr_chassis_slot' => 'USR-Chassis-Slot', - 'usr_final_tx_link_data_r' => 'USR-Final-Tx-Link-Data-Rate', - 'usr_nfas_id' => 'USR-NFAS-ID', - 'called_station_id' => 'Called-Station-Id', - 'login_lat_port' => 'Login-LAT-Port', - 'ascend_dialed_number' => 'Ascend-Dialed-Number', - 'h323_credit_amount' => 'h323-credit-amount', - 'tunnel_local_name' => 'Tunnel_Local_Name', - 'framed_ip_netmask' => 'Framed-IP-Netmask', - 'client_port_id' => 'Client-Port-Id', - 'bg_span_dis' => 'BG_Span_Dis', - 'multi_link_flag' => 'Multi-Link-Flag', - 'bind_sub_user_at_context' => 'Bind_Sub_User_At_Context', - 'usr_ipx_routing' => 'USR-IPX-Routing', - 'ascend_fr_nailed_grp' => 'Ascend-FR-Nailed-Grp', - 'ascend_pre_output_octets' => 'Ascend-Pre-Output-Octets', - 'pppoe_url' => 'PPPOE_URL', - 'ascend_ara_pw' => 'Ascend-Ara-PW', - 'acc_callback_mode' => 'Acc-Callback-Mode', - 'usr_server_time' => 'USR-Server-Time', - 'ascend_seconds_of_histor' => 'Ascend-Seconds-Of-History', - 'ns_mta_md5_password' => 'NS-MTA-MD5-Password', - 'tunnel_server_endpoint' => 'Tunnel-Server-Endpoint', - 'usr_channel' => 'USR-Channel', - 'ascend_dsl_cir_recv_limi' => 'Ascend-Dsl-CIR-Recv-Limit', - 'acct_session_start_time' => 'Acct-Session-Start-Time', - 'ascend_send_passwd' => 'Ascend-Send-Passwd', - 'ascend_num_in_multilink' => 'Ascend-Num-In-Multilink', - 'usr_ip_rip_policies' => 'USR-IP-RIP-Policies', + 'login_ip_host' => 'Login-IP-Host', + 'ascend_netware_timeout' => 'Ascend-Netware-timeout', + 'bind_sub_user_at_context' => 'Bind-Sub-User-At-Context', 'vendor_specific' => 'Vendor-Specific', - 'x_ascend_event_type' => 'X-Ascend-Event-Type', - 'lac_real_port_type' => 'LAC_Real_Port_Type', - 'x_ascend_modem_portno' => 'X-Ascend-Modem-PortNo', - 'usr_originate_answer_mod' => 'USR-Originate-Answer-Mode', - 'framed_ipx_network' => 'Framed-IPX-Network', - 'ascend_modem_slotno' => 'Ascend-Modem-SlotNo', - 'ms_mppe_encryption_type' => 'MS-MPPE-Encryption-Type', - 'annex_cli_command' => 'Annex-CLI-Command', - 'acct_status_type' => 'Acct-Status-Type', - 'usr_et_bridge_call_outpu' => 'USR-ET-Bridge-Call-Output-Filte', - 'usr_pw_vpn_id' => 'USR-PW_VPN_ID', - 'usr_sap_filter_in' => 'USR-SAP-Filter-In', - 'usr_rad_multicast_routin' => 'USR-Rad-Multicast-Routing-Proto', - 'annex_audit_level' => 'Annex-Audit-Level', - 'x_ascend_shared_profile_' => 'X-Ascend-Shared-Profile-Enable', - 'ascend_dial_number' => 'Ascend-Dial-Number', - 'ascend_link_compression' => 'Ascend-Link-Compression', - 'usr_event_date_time' => 'USR-Event-Date-Time', - 'usr_mp_edo_hiper' => 'USR-MP-EDO-HIPER', - 'usr_re_chap_timeout' => 'USR-Re-Chap-Timeout', - 'x_ascend_third_prompt' => 'X-Ascend-Third-Prompt', - 'x_ascend_ppp_vj_1172' => 'X-Ascend-PPP-VJ-1172', - 'annex_disconnect_reason' => 'Annex-Disconnect-Reason', - 'ascend_fr_svc_addr' => 'Ascend-FR-SVC-Addr', - 'nas_real_port' => 'NAS_Real_Port', - 'usr_power_supply_number' => 'USR-Power-Supply-Number', - 'ms_secondary_dns_server' => 'MS-Secondary-DNS-Server', - 'ascend_port_redir_server' => 'Ascend-Port-Redir-Server', - 'ascend_x25_pad_alias_1' => 'Ascend-X25-Pad-Alias-1', - 'x_ascend_fcp_parameter' => 'X-Ascend-FCP-Parameter', - 'ascend_x25_pad_alias_2' => 'Ascend-X25-Pad-Alias-2', - 'ascend_ipsec_profile' => 'Ascend-IPSEC-Profile', - 'ascend_x25_pad_alias_3' => 'Ascend-X25-Pad-Alias-3', + 'ascend_fr_direct_dlci' => 'Ascend-FR-Direct-DLCI', + 'ascend_qos_upstream' => 'Ascend-QOS-Upstream', + 'aat_user_mac_address' => 'AAT-User-MAC-Address', + 'source_validation' => 'Source-Validation', + 'x_ascend_token_expiry' => 'X-Ascend-Token-Expiry', + 'altiga_ipsec_user_group_' => 'Altiga-IPSec-User-Group-Lock-G', + 'ascend_dec_channel_count' => 'Ascend-Dec-Channel-Count', + 'assigned_ip_addrest' => 'Assigned_IP_Address', + 'usr_local_framed_ip_addr' => 'USR-Local-Framed-IP-Addr', + 'usr_service_option' => 'USR-Service-Option', + 'usr_transmit_acc_map' => 'USR-Transmit-Acc-Map', + 'ascend_fr_direct' => 'Ascend-FR-Direct', + 'usr_final_rx_link_data_r' => 'USR-Final-Rx-Link-Data-Rate', + 'x_ascend_expect_callback' => 'X-Ascend-Expect-Callback', + 'x_ascend_disconnect_caus' => 'X-Ascend-Disconnect-Cause', + 'acc_ml_damping_factor' => 'Acc-ML-Damping-Factor', + 'framed_netmask' => 'Framed-Netmask', + 'usr_connect_speed' => 'USR-Connect-Speed', + 'x_ascend_home_agent_ip_a' => 'X-Ascend-Home-Agent-IP-Addr', + 'usr_disconnect_cause_ind' => 'USR-Disconnect-Cause-Indicator', + 'bg_span_dis' => 'BG-Span-Dis', + 'cisco_multilink_id' => 'Cisco-Multilink-ID', + 'tunnel_max_tunnels' => 'Tunnel-Max-Tunnels', + 'ascend_dsl_downstream_li' => 'Ascend-Dsl-Downstream-Limit', + 'ascend_multilink_id' => 'Ascend-Multilink-ID', + 'altiga_ipsec_default_dom' => 'Altiga-IPSec-Default-Domain-G', + 'ascend_dhcp_reply' => 'Ascend-DHCP-Reply', + 'login_ipv6_host' => 'Login-IPv6-Host', + 'ascend_x25_cug' => 'Ascend-X25-Cug', + 'shiva_network_protocols' => 'Shiva-Network-Protocols', + 'cvpn3000_ipsec_mode_conf' => 'CVPN3000-IPSec-Mode-Config', + 'extreme_netlogin_vlan' => 'Extreme-Netlogin-Vlan', + 'ascend_ara_pw' => 'Ascend-Ara-PW', + 'tunnel_l2f_second_passwo' => 'Tunnel-L2F-Second-Password', + 'altiga_sep_card_assignme' => 'Altiga-SEP-Card-Assignment-G/U', + 'ip_host_addr' => 'Ip-Host-Addr', + 'le_ip_gateway' => 'LE-IP-Gateway', 'usr_mobile_numbytes_txed' => 'USR-Mobile-NumBytes-Txed', - 'ascend_atm_vpi' => 'Ascend-ATM-Vpi', - 'annex_input_filter' => 'Annex-Input-Filter', - 'menu' => 'Menu', - 'x_ascend_route_ip' => 'X-Ascend-Route-IP', - 'usr_rmmie_num_of_updates' => 'USR-RMMIE-Num-Of-Updates', + 'altiga_ipsec_allow_passw' => 'Altiga-IPSec-Allow-Passwd-Store-G/U', + 'itk_users_default_entry' => 'ITK-Users-Default-Entry', + 'quintum_h323_redirect_nu' => 'Quintum-h323-redirect-number', + 'x_ascend_fr_t392' => 'X-Ascend-FR-T392', + 'acc_igmp_version' => 'Acc-Igmp-Version', + 'cisco_pre_output_packets' => 'Cisco-Pre-Output-Packets', + 'tunnel_group' => 'Tunnel-Group', + 'x_ascend_home_agent_udp_' => 'X-Ascend-Home-Agent-UDP-Port', + 'cvpn3000_tunneling_proto' => 'CVPN3000-Tunneling-Protocols', + 'usr_igmp_maximum_respons' => 'USR-IGMP-Maximum-Response-Time', + 'bind_sub_password' => 'Bind-Sub-Password', + 'eap_message' => 'EAP-Message', + 'exec_program' => 'Exec-Program', + 'cvpn3000_reqrd_client_fx' => 'CVPN3000-Reqrd-Client-Fw-Product-Code', + 'bg_path_cost' => 'BG-Path-Cost', + 'usr_modem_training_time' => 'USR-Modem-Training-Time', + 'auth_type' => 'Auth-Type', + 'itk_acct_serv_prot' => 'ITK-Acct-Serv-Prot', + 'x_ascend_ipx_route' => 'X-Ascend-IPX-Route', + 'altiga_primary_dns_g' => 'Altiga-Primary-DNS-G', + 'ascend_cbcp_enable' => 'Ascend-CBCP-Enable', + 'ms_mppe_encryption_polic' => 'MS-MPPE-Encryption-Policy', + 'annex_unauthenticated_ti' => 'Annex-Unauthenticated-Time', + 'annex_begin_receive_line' => 'Annex-Begin-Receive-Line-Level', + 'ascend_atm_direct_profil' => 'Ascend-ATM-Direct-Profile', + 'redcreek_tunneled_dns_se' => 'RedCreek-Tunneled-DNS-Server', + 'ascend_redirect_number' => 'Ascend-Redirect-Number', + 'h323_credit_time' => 'h323-credit-time', + 'cvx_idle_limit' => 'CVX-Idle-Limit', + 'ascend_appletalk_route' => 'Ascend-Appletalk-Route', + 'aat_ip_tos' => 'AAT-IP-TOS', + 'cvx_ppp_address' => 'CVX-PPP-Address', + 'aat_data_filter' => 'AAT-Data-Filter', + 'cvx_primary_dns' => 'CVX-Primary-DNS', + 'shiva_link_protocol' => 'Shiva-Link-Protocol', + 'x_ascend_fr_circuit_name' => 'X-Ascend-FR-Circuit-Name', + 'usr_appletalk' => 'USR-Appletalk', + 'client_id' => 'Client-Id', + 'tunnel_algorithn' => 'Tunnel_Algorithm', + 'aat_assign_ip_pool' => 'AAT-Assign-IP-Pool', + 'quintum_h323_incoming_co' => 'Quintum-h323-incoming-conf-id', + 'aat_atm_vpi' => 'AAT-ATM-VPI', + 'annex_output_filter' => 'Annex-Output-Filter', + 'pvc_circuit_padding' => 'PVC-Circuit-Padding', + 'usr_ipx_call_output_filt' => 'USR-IPX-Call-Output-Filter', + 'usr_rmmie_planned_discon' => 'USR-RMMIE-Planned-Disconnect', + 'session_error_msh' => 'Session_Error_Msg', + 'usr_rad_multicast_routin' => 'USR-Rad-Multicast-Routing-Ttl', + 'h323_time_and_day' => 'h323-time-and-day', + 'cvpn3000_ipsec_backup_se' => 'CVPN3000-IPSec-Backup-Servers', + 'termination_action' => 'Termination-Action', + 'cvpn3000_ipsec_client_fx' => 'CVPN3000-IPSec-Client-Fw-Filter-Opt', + 'aat_client_primary_dnt' => 'AAT-Client-Primary-DNS', + 'acct_tunnel_packets_lost' => 'Acct-Tunnel-Packets-Lost', + 'x_ascend_modem_portno' => 'X-Ascend-Modem-PortNo', + 'framed_filter_id' => 'Framed-Filter-Id', + 'usr_ccp_algorithm' => 'USR-CCP-Algorithm', + 'quintum_h323_preferred_l' => 'Quintum-h323-preferred-lang', + 'ascend_fr_link_status_dl' => 'Ascend-FR-Link-Status-DLCI', + 'ascend_token_expiry' => 'Ascend-Token-Expiry', + 'itk_auth_req_type' => 'ITK-Auth-Req-Type', + 'acc_modem_error_protocol' => 'Acc-Modem-Error-Protocol', 'acc_request_type' => 'Acc-Request-Type', - 'ascend_dhcp_reply' => 'Ascend-DHCP-Reply', - 'usr_number_of_upshifts' => 'USR-Number-of-Upshifts', - 'usr_rmmie_firmware_versi' => 'USR-RMMIE-Firmware-Version', - 'bind_bypass_context' => 'Bind_Bypass_Context', - 'ascend_dialout_allowed' => 'Ascend-Dialout-Allowed', - 'annex_tunnel_authen_type' => 'Annex-Tunnel-Authen-Type', - 'x_ascend_bridge' => 'X-Ascend-Bridge', - 'ascend_client_secondary_' => 'Ascend-Client-Secondary-WINS', - 'erx_local_loopback_inter' => 'ERX-Local-Loopback-Interface', - 'acct_input_gigawords' => 'Acct-Input-Gigawords', - 'usr_equalization_type' => 'USR-Equalization-Type', - 'usr_port_tap_format' => 'USR-Port-Tap-Format', - 'x_ascend_ppp_async_map' => 'X-Ascend-PPP-Async-Map', - 'acc_ipx_compression' => 'Acc-Ipx-Compression', - 'ascend_nas_port_format' => 'Ascend-NAS-Port-Format', - 'acc_modem_modulation_typ' => 'Acc-Modem-Modulation-Type', - 'ascend_modem_portno' => 'Ascend-Modem-PortNo', - 'usr_et_bridge_output_fil' => 'USR-ET-Bridge-Output-Filter', - 'ascend_ipx_header_compre' => 'Ascend-IPX-Header-Compression', - 'framed_appletalk_link' => 'Framed-AppleTalk-Link', - 'x_ascend_receive_secret' => 'X-Ascend-Receive-Secret', - 'ascend_route_ipx' => 'Ascend-Route-IPX', - 'ascend_user_acct_type' => 'Ascend-User-Acct-Type', - 'ascend_token_idle' => 'Ascend-Token-Idle', - 'framed_ip_address' => 'Framed-IP-Address', - 'ascend_call_block_durati' => 'Ascend-Call-Block-Duration', - 'ascend_ppp_address' => 'Ascend-PPP-Address', - 'usr_mbi_ct_pri_card_slot' => 'USR-Mbi_Ct_PRI_Card_Slot', - 'x_ascend_dec_channel_cou' => 'X-Ascend-Dec-Channel-Count', - 'x_ascend_send_auth' => 'X-Ascend-Send-Auth', - 'usr_characters_received' => 'USR-Characters-Received', - 'usr_pw_tunnel_authentica' => 'USR-PW_Tunnel_Authentication', - 'usr_call_end_time' => 'USR-Call-End-Time', - 'x_ascend_dialout_allowed' => 'X-Ascend-Dialout-Allowed', - 'x_ascend_call_attempt_li' => 'X-Ascend-Call-Attempt-Limit', - 'initial_modulation_type' => 'Initial-Modulation-Type', - 'usr_packet_bus_session' => 'USR-Packet-Bus-Session', - 'x_ascend_ipx_node_addr' => 'X-Ascend-IPX-Node-Addr', + 'usr_last_number_dialed_i' => 'USR-Last-Number-Dialed-In-DNIS', + 'x_ascend_ipx_peer_mode' => 'X-Ascend-IPX-Peer-Mode', 'ascend_ppp_vj_slot_comp' => 'Ascend-PPP-VJ-Slot-Comp', - 'ascend_menu_item' => 'Ascend-Menu-Item', - 'x_ascend_fr_link_mgt' => 'X-Ascend-FR-Link-Mgt', - 'usr_rmmie_serial_number' => 'USR-RMMIE-Serial-Number', - 'message_authenticator' => 'Message-Authenticator', - 'usr_dte_data_idle_timout' => 'USR-DTE-Data-Idle-Timout', - 'usr_port_tap_facility' => 'USR-Port-Tap-Facility', - 'acc_ml_mlx_admin_state' => 'Acc-ML-MLX-Admin-State', - 'usr_modem_group' => 'USR-Modem-Group', - 'x_ascend_callback' => 'X-Ascend-Callback', - 'acct_input_packets_64' => 'Acct_Input_Packets_64', - 'ascend_third_prompt' => 'Ascend-Third-Prompt', - 'configuration_token' => 'Configuration-Token', - 'x_ascend_fr_nailed_grp' => 'X-Ascend-FR-Nailed-Grp', - 'acct_output_octets_64' => 'Acct_Output_Octets_64', - 'h323_time_and_day' => 'h323-time-and-day', - 'ascend_port_redir_portnu' => 'Ascend-Port-Redir-Portnum', - 'acct_interim_interval' => 'Acct-Interim-Interval', - 'ascend_uu_info' => 'Ascend-UU-Info', + 'cisco_presession_time' => 'Cisco-PreSession-Time', + 'usr_chat_script_name' => 'USR-Chat-Script-Name', + 'tunnel_session_auti' => 'Tunnel_Session_Auth', + 'ascend_fr_circuit_name' => 'Ascend-FR-Circuit-Name', + 'ascend_expect_callback' => 'Ascend-Expect-Callback', + 'framed_mtu' => 'Framed-MTU', 'usr_pw_vpn_name' => 'USR-PW_VPN_Name', - 'ascend_maximum_call_dura' => 'Ascend-Maximum-Call-Duration', - 'ascend_atm_direct_profil' => 'Ascend-ATM-Direct-Profile', - 'acc_input_errors' => 'Acc-Input-Errors', - 'bind_dot1q_port' => 'Bind_Dot1q_Port', - 'ascend_first_dest' => 'Ascend-First-Dest', - 'x_ascend_if_netmask' => 'X-Ascend-IF-Netmask', - 'tunnel_session_auth_serv' => 'Tunnel_Session_Auth_Service_Grp', - 'annex_local_ip_address' => 'Annex-Local-IP-Address', - 'termination_menu' => 'Termination-Menu', - 'ms_chap2_cpw' => 'MS-CHAP2-CPW', - 'ascend_mpp_idle_percent' => 'Ascend-MPP-Idle-Percent', - 'usr_characters_sent' => 'USR-Characters-Sent', - 'eap_message' => 'EAP-Message', - 'acct_delay_time' => 'Acct-Delay-Time', - 'ascend_remote_fw' => 'Ascend-Remote-FW', - 'x_ascend_tunneling_proto' => 'X-Ascend-Tunneling-Protocol', - 'shiva_session_id' => 'Shiva-Session-Id', - 'usr_igmp_query_interval' => 'USR-IGMP-Query-Interval', - 'usr_accm_type' => 'USR-ACCM-Type', - 'usr_call_terminate_in_gm' => 'USR-Call-Terminate-in-GMT', - 'usr_rad_location_type' => 'USR-Rad-Location-Type', - 'ascend_filter' => 'Ascend-Filter', - 'ascend_primary_home_agen' => 'Ascend-Primary-Home-Agent', - 'x_ascend_user_acct_host' => 'X-Ascend-User-Acct-Host', - 'chap_challenge' => 'CHAP-Challenge', - 'acct_output_packets_64' => 'Acct_Output_Packets_64', - 'bind_auth_max_sessions' => 'Bind_Auth_Max_Sessions', - 'cisco_pre_output_octets' => 'Cisco-Pre-Output-Octets', - 'x_ascend_fr_direct' => 'X-Ascend-FR-Direct', - 'x_ascend_client_secondar' => 'X-Ascend-Client-Secondary-DNS', - 'usr_rmmie_pwrlvl_nearech' => 'USR-RMMIE-PwrLvl-NearEcho-Canc', - 'ascend_bridge_address' => 'Ascend-Bridge-Address', - 'user_name' => 'User-Name', - 'usr_rmmie_firmware_build' => 'USR-RMMIE-Firmware-Build-Date', - 'ms_chap_mppe_keys' => 'MS-CHAP-MPPE-Keys', - 'usr_number_of_characters' => 'USR-Number-Of-Characters-Lost', - 'usr_physical_state' => 'USR-Physical-State', - 'x_ascend_assign_ip_serve' => 'X-Ascend-Assign-IP-Server', - 'bind_int_context' => 'Bind_Int_Context', - 'erx_tunnel_virtual_route' => 'ERX-Tunnel-Virtual-Router', - 'ascend_xmit_rate' => 'Ascend-Xmit-Rate', - 'usr_secondary_dns_server' => 'USR-Secondary_DNS_Server', - 'ascend_dsl_rate_mode' => 'Ascend-Dsl-Rate-Mode', + 'nomadix_ip_upsell' => 'Nomadix-IP-Upsell', + 'ascend_nas_port_format' => 'Ascend-NAS-Port-Format', + 'usr_dtr_true_timeout' => 'USR-DTR-True-Timeout', + 'shasta_vpn_name' => 'Shasta-VPN-Name', + 'connect_rate' => 'Connect-Rate', + 'ascend_third_prompt' => 'Ascend-Third-Prompt', + 'cabletron_protocol_enabl' => 'Cabletron-Protocol-Enable', + 'annex_pre_input_octets' => 'Annex-Pre-Input-Octets', + 'cvx_modem_error_correcti' => 'CVX-Modem-Error-Correction', + 'cvx_ss7_session_id_type' => 'CVX-SS7-Session-ID-Type', + 'called_station_id' => 'Called-Station-Id', + 'itk_ddi' => 'ITK-DDI', + 'usr_pw_cutoff' => 'USR-PW_Cutoff', 'ascend_data_rate' => 'Ascend-Data-Rate', - 'realm' => 'Realm', - 'usr_ipx_call_input_filte' => 'USR-IPX-Call-Input-Filter', - 'ascend_ipx_route' => 'Ascend-IPX-Route', - 'usr_failure_to_connect_r' => 'USR-Failure-to-Connect-Reason', - 'x_ascend_home_network_na' => 'X-Ascend-Home-Network-Name', - 'acc_nbns_server_pri' => 'Acc-Nbns-Server-Pri', - 'usr_modulation_type' => 'USR-Modulation-Type', - 'service_type' => 'Service-Type', - 'ascend_callback_delay' => 'Ascend-Callback-Delay', - 'ascend_owner_ip_addr' => 'Ascend-Owner-IP-Addr', - 'x_ascend_handle_ipx' => 'X-Ascend-Handle-IPX', - 'usr_connect_term_reason' => 'USR-Connect-Term-Reason', - 'x_ascend_multicast_rate_' => 'X-Ascend-Multicast-Rate-Limit', - 'h323_disconnect_time' => 'h323-disconnect-time', - 'acc_ip_gateway_sec' => 'Acc-Ip-Gateway-Sec', - 'usr_number_of_blers' => 'USR-Number-of-Blers', - 'x_ascend_fr_type' => 'X-Ascend-FR-Type', - 'ascend_assign_ip_pool' => 'Ascend-Assign-IP-Pool', - 'ascend_qos_upstream' => 'Ascend-QOS-Upstream', - 'usr_nas_type' => 'USR-NAS-Type', - 'acc_dial_port_index' => 'Acc-Dial-Port-Index', - 'usr_initial_tx_link_data' => 'USR-Initial-Tx-Link-Data-Rate', - 'ascend_fr_type' => 'Ascend-FR-Type', - 'usr_mbi_ct_tdm_time_slot' => 'USR-Mbi_Ct_TDM_Time_Slot', - 'usr_rmmie_pwrlvl_xmit_lv' => 'USR-RMMIE-PwrLvl-Xmit-Lvl', - 'erx_atm_service_category' => 'ERX-Atm-Service-Category', - 'usr_appletalk' => 'USR-Appletalk', - 'usr_send_script1' => 'USR-Send-Script1', - 'usr_send_script2' => 'USR-Send-Script2', - 'usr_send_script3' => 'USR-Send-Script3', - 'usr_ospf_addressless_ind' => 'USR-OSPF-Addressless-Index', - 'acct_input_packets' => 'Acct-Input-Packets', - 'usr_send_script4' => 'USR-Send-Script4', - 'usr_send_script5' => 'USR-Send-Script5', - 'usr_send_script6' => 'USR-Send-Script6', - 'usr_service_option' => 'USR-Service-Option', - 'ascend_dropped_octets' => 'Ascend-Dropped-Octets', - 'usr_ip' => 'USR-IP', - 'usr_tunnel_security' => 'USR-Tunnel-Security', - 'acc_acct_on_off_reason' => 'Acc-Acct-On-Off-Reason', - 'shiva_compression_type' => 'Shiva-Compression-Type', - 'ascend_pw_warntime' => 'Ascend-PW-Warntime', - 'usr_security_resp_limit' => 'USR-Security-Resp-Limit', + 'acct_input_packets_65' => 'Acct_Input_Packets_64', + 'x_ascend_ts_idle_mode' => 'X-Ascend-TS-Idle-Mode', 'ascend_x25_pad_prompt' => 'Ascend-X25-Pad-Prompt', - 'cisco_asing_ip_pool' => 'Cisco-Asing-IP-Pool', - 'acc_route_policy' => 'Acc-Route-Policy', - 'annex_local_username' => 'Annex-Local-Username', - 'x_ascend_call_by_call' => 'X-Ascend-Call-By-Call', - 'ascend_calling_id_screen' => 'Ascend-Calling-Id-Screening', - 'x_ascend_dhcp_pool_numbe' => 'X-Ascend-DHCP-Pool-Number', - 'nas_port_type' => 'NAS-Port-Type', - 'ascend_route_ip' => 'Ascend-Route-IP', - 'ascend_client_gateway' => 'Ascend-Client-Gateway', - 'ascend_qos_downstream' => 'Ascend-QOS-Downstream', - 'ms_bap_usage' => 'MS-BAP-Usage', - 'usr_vts_session_key' => 'USR-VTS-Session-Key', - 'usr_receive_acc_map' => 'USR-Receive-Acc-Map', - 'ascend_expect_callback' => 'Ascend-Expect-Callback', - 'password' => 'Password', - 'packet_type' => 'Packet-Type', - 'ascend_remote_addr' => 'Ascend-Remote-Addr', - 'ascend_recv_name' => 'Ascend-Recv-Name', - 'ms_acct_eap_type' => 'MS-Acct-EAP-Type', - 'usr_filter_zones' => 'USR-Filter-Zones', - 'annex_output_filter' => 'Annex-Output-Filter', - 'usr_rmmie_rcv_tot_pwrlvl' => 'USR-RMMIE-Rcv-Tot-PwrLvl', - 'usr_mp_mrru' => 'USR-MP-MRRU', + 'x_ascend_dhcp_reply' => 'X-Ascend-DHCP-Reply', + 'acc_nbns_server_pri' => 'Acc-Nbns-Server-Pri', + 'post_auth_type' => 'Post-Auth-Type', 'ascend_call_filter' => 'Ascend-Call-Filter', - 'usr_keypress_timeout' => 'USR-Keypress-Timeout', - 'usr_modem_setup_time' => 'USR-Modem-Setup-Time', - 'acct_authentic' => 'Acct-Authentic', - 'pppoe_motm' => 'PPPOE_MOTM', - 'x_ascend_expect_callback' => 'X-Ascend-Expect-Callback', - 'erx_atm_scr' => 'ERX-Atm-SCR', - 'erx_address_pool_name' => 'ERX-Address-Pool-Name', + 'acc_tunnel_secret' => 'Acc-Tunnel-Secret', + 'colubris_avpair' => 'Colubris-AVPair', + 'bind_int_context' => 'Bind-Int-Context', + 'annex_logical_channel_nu' => 'Annex-Logical-Channel-Number', + 'erx_virtual_router_name' => 'ERX-Virtual-Router-Name', + 'wispr_redirection_url' => 'WISPr-Redirection-URL', + 'bintec_ipextiftable' => 'BinTec-ipExtIfTable', + 'crypt_password' => 'Crypt-Password', 'challenge_state' => 'Challenge-State', - 'usr_multicast_proxy' => 'USR-Multicast-Proxy', - 'framed_filter_id' => 'Framed-Filter-Id', - 'add_suffix' => 'Add-Suffix', + 'x_ascend_pre_input_packe' => 'X-Ascend-Pre-Input-Packets', + 'altiga_ipsec_l2l_keepali' => 'Altiga-IPSec-L2L-Keepalives-G', + 'x_ascend_dhcp_maximum_le' => 'X-Ascend-DHCP-Maximum-Leases', + 'acc_dialout_auth_passwor' => 'Acc-Dialout-Auth-Password', + 'itk_ip_pool' => 'ITK-IP-Pool', + 'pvc_profile_namf' => 'PVC_Profile_Name', + 'x_ascend_user_acct_host' => 'X-Ascend-User-Acct-Host', + 'strip_user_name' => 'Strip-User-Name', + 'itk_ppp_client_server_mo' => 'ITK-PPP-Client-Server-Mode', + 'usr_mbi_ct_bchannel_used' => 'USR-Mbi_Ct_BChannel_Used', + 'x_ascend_route_ip' => 'X-Ascend-Route-IP', + 'ascend_seconds_of_histor' => 'Ascend-Seconds-Of-History', + 'cvx_data_rate' => 'CVX-Data-Rate', + 'ascend_x25_profile_name' => 'Ascend-X25-Profile-Name', + 'itk_ftp_auth_ip' => 'ITK-Ftp-Auth-IP', + 'cisco_control_info' => 'Cisco-Control-Info', + 'cvpn3000_secondary_wins' => 'CVPN3000-Secondary-WINS', + 'usr_call_type' => 'USR-Call-Type', + 'x_ascend_user_acct_base' => 'X-Ascend-User-Acct-Base', + 'acct_mcast_in_packett' => 'Acct_Mcast_In_Packets', + 'ns_vsys_name' => 'NS-VSYS-Name', + 'acct_output_gigawords' => 'Acct-Output-Gigawords', + 'bind_typf' => 'Bind_Type', + 'bintec_ipqostable' => 'BinTec-ipQoSTable', + 'bintec_ipxstaticservtabl' => 'BinTec-ipxStaticServTable', + 'cvpn3000_l2tp_mppc_compr' => 'CVPN3000-L2TP-MPPC-Compression', + 'login_lat_port' => 'Login-LAT-Port', + 'usr_call_arrival_in_gmt' => 'USR-Call-Arrival-in-GMT', + 'acct_mcast_in_octets' => 'Acct-Mcast-In-Octets', + 'erx_sa_validate' => 'ERX-Sa-Validate', + 'ascend_service_type' => 'Ascend-Service-Type', + 'usr_pw_vpn_gateway' => 'USR-PW_VPN_Gateway', + 'acc_ip_compression' => 'Acc-Ip-Compression', + 'ascend_fr_dce_n392' => 'Ascend-FR-DCE-N392', + 'bintec_ipxcirctable' => 'BinTec-ipxCircTable', + 'lac_real_port_type' => 'LAC-Real-Port-Type', + 'ascend_client_primary_dn' => 'Ascend-Client-Primary-DNS', + 'acct_session_start_time' => 'Acct-Session-Start-Time', + 'ascend_if_netmask' => 'Ascend-IF-Netmask', + 'ms_chap_nt_enc_pw' => 'MS-CHAP-NT-Enc-PW', + 'ms_mppe_encryption_types' => 'MS-MPPE-Encryption-Types', + 'cisco_fax_process_abort_' => 'Cisco-Fax-Process-Abort-Flag', + 'mcast_maxgroups' => 'Mcast-MaxGroups', + 'annex_end_receive_line_l' => 'Annex-End-Receive-Line-Level', + 'usr_ipx_call_input_filte' => 'USR-IPX-Call-Input-Filter', + 'usr_back_channel_data_ra' => 'USR-Back-Channel-Data-Rate', + 'ascend_cache_time' => 'Ascend-Cache-Time', + 'x_ascend_data_svc' => 'X-Ascend-Data-Svc', + 'usr_re_chap_timeout' => 'USR-Re-Chap-Timeout', + 'bintec_bibodialtable' => 'BinTec-biboDialTable', + 'annex_connect_progress' => 'Annex-Connect-Progress', + 'x_ascend_ppp_vj_1172' => 'X-Ascend-PPP-VJ-1172', + 'usr_igmp_routing' => 'USR-IGMP-Routing', + 'x_ascend_ip_pool_definit' => 'X-Ascend-IP-Pool-Definition', + 'h323_prompt_id' => 'h323-prompt-id', + 'foundry_command_string' => 'Foundry-Command-String', + 'le_terminate_detail' => 'LE-Terminate-Detail', + 'cvpn3000_pptp_encryption' => 'CVPN3000-PPTP-Encryption', + 'quintum_h323_disconnect_' => 'Quintum-h323-disconnect-time', + 'acc_ml_clear_threshold' => 'Acc-ML-Clear-Threshold', + 'x_ascend_ip_direct' => 'X-Ascend-IP-Direct', + 'usr_ip_call_input_filter' => 'USR-IP-Call-Input-Filter', + 'x_ascend_data_rate' => 'X-Ascend-Data-Rate', + 'nas_port' => 'NAS-Port', + 'ascend_client_secondary_' => 'Ascend-Client-Secondary-WINS', 'ascend_auth_type' => 'Ascend-Auth-Type', - 'session_timeout' => 'Session-Timeout', - 'ascend_callback' => 'Ascend-Callback', - 'usr_chat_script_name' => 'USR-Chat-Script-Name', - 'port_message' => 'Port-Message', - 'acct_output_packets' => 'Acct-Output-Packets', - 'ascend_session_svr_key' => 'Ascend-Session-Svr-Key', - 'login_tcp_port' => 'Login-TCP-Port', - 'erx_tunnel_password' => 'ERX-Tunnel-Password', - 'shasta_user_privilege' => 'Shasta-User-Privilege', - 'usr_secondary_nbns_serve' => 'USR-Secondary_NBNS_Server', - 'usr_security_login_limit' => 'USR-Security-Login-Limit', - 'usr_start_time' => 'USR-Start-Time', - 'acc_access_partition' => 'Acc-Access-Partition', - 'versanet_termination_cau' => 'Versanet-Termination-Cause', - 'x_ascend_call_block_dura' => 'X-Ascend-Call-Block-Duration', - 'mcast_maxgroups' => 'Mcast_MaxGroups', - 'ascend_user_acct_base' => 'Ascend-User-Acct-Base', - 'usr_vpn_gw_location_id' => 'USR-VPN-GW-Location-Id', - 'usr_block_error_count_li' => 'USR-Block-Error-Count-Limit', - 'ascend_telnet_profile' => 'Ascend-Telnet-Profile', - 'ascend_port_redir_protoc' => 'Ascend-Port-Redir-Protocol', - 'ascend_call_by_call' => 'Ascend-Call-By-Call', - 'usr_disconnect_cause_ind' => 'USR-Disconnect-Cause-Indicator', - 'x_ascend_fr_linkup' => 'X-Ascend-FR-LinkUp', - 'ascend_billing_number' => 'Ascend-Billing-Number', - 'usr_ds0s' => 'USR-DS0s', - 'usr_at_zip_output_filter' => 'USR-AT-Zip-Output-Filter', - 'ascend_user_acct_port' => 'Ascend-User-Acct-Port', - 'login_port' => 'Login-Port', - 'arap_security' => 'ARAP-Security', - 'tunnel_deadtime' => 'Tunnel_Deadtime', - 'ascend_user_acct_time' => 'Ascend-User-Acct-Time', - 'ms_chap_challenge' => 'MS-CHAP-Challenge', - 'ascend_x25_rpoa' => 'Ascend-X25-Rpoa', - 'login_time' => 'Login-Time', + 'x_ascend_preempt_limit' => 'X-Ascend-Preempt-Limit', + 'cvx_xmit_rate' => 'CVX-Xmit-Rate', + 'annex_transmitted_packet' => 'Annex-Transmitted-Packets', + 'h323_credit_amount' => 'h323-credit-amount', + 'usr_reply_script1' => 'USR-Reply-Script1', 'current_time' => 'Current-Time', - 'login_service' => 'Login-Service', - 'ascend_menu_selector' => 'Ascend-Menu-Selector', - 'ascend_bacp_enable' => 'Ascend-BACP-Enable', - 'shiva_link_speed' => 'Shiva-Link-Speed', - 'ascend_private_route_tab' => 'Ascend-Private-Route-Table-ID', + 'cisco_xmit_rate' => 'Cisco-Xmit-Rate', 'x_ascend_session_svr_key' => 'X-Ascend-Session-Svr-Key', - 'ascend_data_filter' => 'Ascend-Data-Filter', - 'ascend_target_util' => 'Ascend-Target-Util', - 'shiva_function' => 'Shiva-Function', - 'usr_pw_usr_ifilter_ip' => 'USR-PW_USR_IFilter_IP', - 'usr_igmp_routing' => 'USR-IGMP-Routing', - 'acc_tunnel_port' => 'Acc-Tunnel-Port', - 'x_ascend_fr_n391' => 'X-Ascend-FR-N391', - 'medium_type' => 'Medium_Type', - 'annex_domain_name' => 'Annex-Domain-Name', - 'ascend_fr_n391' => 'Ascend-FR-N391', - 'callback_number' => 'Callback-Number', - 'usr_chassis_temperature' => 'USR-Chassis-Temperature', - 'dialback_no' => 'Dialback-No', - 'ms_mppe_recv_key' => 'MS-MPPE-Recv-Key', - 'ascend_ipx_alias' => 'Ascend-IPX-Alias', - 'le_nat_inmap' => 'LE-NAT-Inmap', - 'tunnel_police_rate' => 'Tunnel_Police_Rate', - 'acct_terminate_cause' => 'Acct-Terminate-Cause', - 'le_nat_other_session_tim' => 'LE-NAT-Other-Session-Timeout', - 'usr_ip_rip_output_filter' => 'USR-IP-RIP-Output-Filter', - 'exec_program' => 'Exec-Program', - 'h323_disconnect_cause' => 'h323-disconnect-cause', - 'usr_chassis_call_channel' => 'USR-Chassis-Call-Channel', - 'x_ascend_fr_dlci' => 'X-Ascend-FR-DLCI', - 'ms_link_drop_time_limit' => 'MS-Link-Drop-Time-Limit', - 'acc_callback_num_valid' => 'Acc-Callback-Num-Valid', - 'cisco_presession_time' => 'Cisco-PreSession-Time', - 'ms_chap_response' => 'MS-CHAP-Response', - 'usr_spoofing' => 'USR-Spoofing', - 'usr_num_fax_pages_proces' => 'USR-Num-Fax-Pages-Processed', - 'ascend_x25_cug' => 'Ascend-X25-Cug', - 'ascend_fr_dlci' => 'Ascend-FR-DLCI', - 'shiva_user_attributes' => 'Shiva-User-Attributes', - 'ms_chap_lm_enc_pw' => 'MS-CHAP-LM-Enc-PW', - 'ascend_transit_number' => 'Ascend-Transit-Number', - 'usr_last_number_dialed_i' => 'USR-Last-Number-Dialed-In-DNIS', - 'usr_ip_saa_filter' => 'USR-IP-SAA-Filter', - 'usr_pw_usr_ifilter_ipx' => 'USR-PW_USR_IFilter_IPX', - 'ascend_remove_seconds' => 'Ascend-Remove-Seconds', - 'le_connect_detail' => 'LE-Connect-Detail', - 'ascend_assign_ip_global_' => 'Ascend-Assign-IP-Global-Pool', - 'proxy_to_realm' => 'Proxy-To-Realm', - 'usr_retrains_requested' => 'USR-Retrains-Requested', - 'h323_remote_address' => 'h323-remote-address', - 'ascend_x25_nui_prompt' => 'Ascend-X25-Nui-Prompt', - 'acc_customer_id' => 'Acc-Customer-Id', - 'ms_chap2_response' => 'MS-CHAP2-Response', - 'ascend_host_info' => 'Ascend-Host-Info', - 'annex_addr_resolution_se' => 'Annex-Addr-Resolution-Servers', - 'x_ascend_multilink_id' => 'X-Ascend-Multilink-ID', - 'login_lat_service' => 'Login-LAT-Service', - 'usr_rmmie_rcv_pwrlvl_330' => 'USR-RMMIE-Rcv-PwrLvl-3300Hz', - 'ascend_event_type' => 'Ascend-Event-Type', - 'ascend_inc_channel_count' => 'Ascend-Inc-Channel-Count', - 'cisco_ppp_async_map' => 'Cisco-PPP-Async-Map', - 'usr_min_compression_size' => 'USR-Min-Compression-Size', - 'ascend_traffic_shaper' => 'Ascend-Traffic-Shaper', - 'ascend_user_acct_key' => 'Ascend-User-Acct-Key', - 'usr_port_tap_output' => 'USR-Port-Tap-Output', - 'ascend_x25_nui' => 'Ascend-X25-Nui', - 'x_ascend_disconnect_caus' => 'X-Ascend-Disconnect-Cause', - 'ascend_cbcp_enable' => 'Ascend-CBCP-Enable', - 'usr_framed_ip_address_po' => 'USR-Framed_IP_Address_Pool_Name', - 'ascend_x25_profile_name' => 'Ascend-X25-Profile-Name', - 'usr_orig_nas_type' => 'USR-Orig-NAS-Type', + 'ascend_authen_alias' => 'Ascend-Authen-Alias', + 'erx_redirect_vr_name' => 'ERX-Redirect-VR-Name', + 'module_success_message' => 'Module-Success-Message', + 'acc_dialout_auth_mode' => 'Acc-Dialout-Auth-Mode', + 'bind_auth_contexu' => 'Bind_Auth_Context', + 'x_ascend_minimum_channel' => 'X-Ascend-Minimum-Channels', + 'usr_event_date_time' => 'USR-Event-Date-Time', + 'x_ascend_ipx_node_addr' => 'X-Ascend-IPX-Node-Addr', + 'cvpn3000_ipsec_over_udp' => 'CVPN3000-IPSec-Over-UDP', + 'x_ascend_user_acct_time' => 'X-Ascend-User-Acct-Time', + 'cisco_email_server_ack_f' => 'Cisco-Email-Server-Ack-Flag', + 'telebit_activate_command' => 'Telebit-Activate-Command', 'acc_output_errors' => 'Acc-Output-Errors', - 'h323_redirect_ip_address' => 'h323-redirect-ip-address', - 'usr_ip_call_output_filte' => 'USR-IP-Call-Output-Filter', - 'cisco_avpair' => 'Cisco-AVPair', - 'usr_slot_connected_to' => 'USR-Slot-Connected-To', - 'framed_route' => 'Framed-Route', - 'ascend_global_call_id' => 'Ascend-Global-Call-Id', - 'x_ascend_seconds_of_hist' => 'X-Ascend-Seconds-Of-History', - 'x_ascend_temporary_rtes' => 'X-Ascend-Temporary-Rtes', - 'h323_currency_type' => 'h323-currency-type', - 'x_ascend_token_expiry' => 'X-Ascend-Token-Expiry', - 'pvc_encapsulation_type' => 'PVC_Encapsulation_Type', - 'x_ascend_pw_lifetime' => 'X-Ascend-PW-Lifetime', + 'juniper_allow_configurat' => 'Juniper-Allow-Configuration', + 'bind_l2tp_tunnel_name' => 'Bind-L2TP-Tunnel-Name', + 'x_ascend_pri_number_type' => 'X-Ascend-PRI-Number-Type', + 'bintec_biboppptable' => 'BinTec-biboPPPTable', + 'le_ipsec_outsource_profi' => 'LE-IPSec-Outsource-Profile', + 'usr_at_zip_input_filter' => 'USR-AT-Zip-Input-Filter', + 'replicate_to_realm' => 'Replicate-To-Realm', + 'annex_mrru' => 'Annex-MRRU', + 'event_timestamp' => 'Event-Timestamp', + 'nokia_sgsn_ip_address' => 'Nokia-SGSN-IP-Address', + 'ascend_pre_input_packets' => 'Ascend-Pre-Input-Packets', + 'cvpn5000_client_assigned' => 'CVPN5000-Client-Assigned-IP', + 'tunnel_dnit' => 'Tunnel_DNIS', + 'h323_call_origin' => 'h323-call-origin', + 'x_ascend_fr_type' => 'X-Ascend-FR-Type', + 'itk_provider_id' => 'ITK-Provider-Id', + 'cvx_ppp_log_mask' => 'CVX-PPP-Log-Mask', + 'x_ascend_token_idle' => 'X-Ascend-Token-Idle', + 'usr_rmmie_pwrlvl_xmit_lv' => 'USR-RMMIE-PwrLvl-Xmit-Lvl', + 'usr_igmp_query_interval' => 'USR-IGMP-Query-Interval', + 'quintum_h323_billing_mod' => 'Quintum-h323-billing-model', + 'ascend_atm_vci' => 'Ascend-ATM-Vci', + 'usr_port_tap_output' => 'USR-Port-Tap-Output', + 'session' => 'Session', + 'itk_welcome_message' => 'ITK-Welcome-Message', + 'cvpn3000_ike_keep_alives' => 'CVPN3000-IKE-Keep-Alives', + 'ascend_uu_info' => 'Ascend-UU-Info', + 'usr_et_bridge_call_outpu' => 'USR-ET-Bridge-Call-Output-Filte', + 'usr_secondary_dns_server' => 'USR-Secondary_DNS_Server', + 'ms_mppe_recv_key' => 'MS-MPPE-Recv-Key', + 'bintec_ripcirctable' => 'BinTec-ripCircTable', + 'acc_dial_port_index' => 'Acc-Dial-Port-Index', + 'cisco_nas_port' => 'Cisco-NAS-Port', + 'itk_username' => 'ITK-Username', + 'usr_send_script1' => 'USR-Send-Script1', + 'cvpn3000_ipsec_ike_peer_' => 'CVPN3000-IPSec-IKE-Peer-ID-Check', + 'ascend_dsl_upstream_limi' => 'Ascend-Dsl-Upstream-Limit', + 'x_ascend_dec_channel_cou' => 'X-Ascend-Dec-Channel-Count', + 'usr_tunnel_security' => 'USR-Tunnel-Security', + 'arap_security' => 'ARAP-Security', + 'tunnel_preference' => 'Tunnel-Preference', + 'cisco_port_used' => 'Cisco-Port-Used', + 'usr_reply_script4' => 'USR-Reply-Script4', + 'cvpn5000_client_real_ip' => 'CVPN5000-Client-Real-IP', + 'usr_rmmie_status' => 'USR-RMMIE-Status', + 'usr_send_script4' => 'USR-Send-Script4', + 'quintum_h323_connect_tim' => 'Quintum-h323-connect-time', + 'annex_syslog_tap' => 'Annex-Syslog-Tap', + 'redcreek_tunneled_hostna' => 'RedCreek-Tunneled-HostName', + 'acc_clearing_location' => 'Acc-Clearing-Location', + 'ascend_access_intercept_' => 'Ascend-Access-Intercept-LEA', + 'annex_disconnect_reason' => 'Annex-Disconnect-Reason', + 'usr_at_input_filter' => 'USR-AT-Input-Filter', + 'usr_auth_mode' => 'USR-Auth-Mode', 'usr_expected_voltage' => 'USR-Expected-Voltage', - 'usr_simplified_v42bis_us' => 'USR-Simplified-V42bis-Usage', - 'shiva_customer_id' => 'Shiva-Customer-Id', - 'usr_compression_algorith' => 'USR-Compression-Algorithm', - 'annex_system_disc_reason' => 'Annex-System-Disc-Reason', + 'shiva_session_id' => 'Shiva-Session-Id', + 'annex_maximum_call_durat' => 'Annex-Maximum-Call-Duration', + 'usr_block_error_count_li' => 'USR-Block-Error-Count-Limit', + 'ascend_owner_ip_addr' => 'Ascend-Owner-IP-Addr', + 'bind_tun_contexu' => 'Bind_Tun_Context', + 'usr_pw_usr_ofilter_ipx' => 'USR-PW_USR_OFilter_IPX', + 'framed_routing' => 'Framed-Routing', + 'annex_primary_nbns_serve' => 'Annex-Primary-NBNS-Server', + 'usr_interface_index' => 'USR-Interface-Index', + 'pam_auth' => 'Pam-Auth', + 'usr_end_time' => 'USR-End-Time', + 'rate_limit_bursu' => 'Rate_Limit_Burst', + 'nomadix_expiration' => 'Nomadix-Expiration', + 'x_ascend_transit_number' => 'X-Ascend-Transit-Number', + 'itk_usergroup' => 'ITK-Usergroup', + 'x_ascend_assign_ip_pool' => 'X-Ascend-Assign-IP-Pool', 'annex_secondary_nbns_ser' => 'Annex-Secondary-NBNS-Server', - 'usr_q931_call_reference_' => 'USR-Q931-Call-Reference-Value', - 'usr_send_password' => 'USR-Send-Password', - 'prompt' => 'Prompt', - 'usr_cusr_hat_script_rule' => 'USR-CUSR-hat-Script-Rules', - 'usr_event_id' => 'USR-Event-Id', - 'usr_ccp_algorithm' => 'USR-CCP-Algorithm', - 'usr_mbi_ct_bchannel_used' => 'USR-Mbi_Ct_BChannel_Used', - 'ascend_svc_enabled' => 'Ascend-SVC-Enabled', - 'framed_mtu' => 'Framed-MTU', - 'acc_reason_code' => 'Acc-Reason-Code', - 'bind_l2tp_flow_control' => 'Bind_L2TP_Flow_Control', + 'bind_dot1q_vlan_tag_id' => 'Bind-Dot1q-Vlan-Tag-Id', + 'ms_secondary_nbns_server' => 'MS-Secondary-NBNS-Server', + 'tunnel_retransmit' => 'Tunnel-Retransmit', + 'acct_tunnel_connection' => 'Acct-Tunnel-Connection', + 'x_ascend_backup' => 'X-Ascend-Backup', + 'xedia_ppp_echo_interval' => 'Xedia-PPP-Echo-Interval', + 'usr_bearer_capabilities' => 'USR-Bearer-Capabilities', + 'shiva_acct_serv_switch' => 'Shiva-Acct-Serv-Switch', + 'acct_authentic' => 'Acct-Authentic', + 'le_nat_other_session_tim' => 'LE-NAT-Other-Session-Timeout', + 'cvpn3000_ipsec_banner2' => 'CVPN3000-IPSec-Banner2', + 'x_ascend_force_56' => 'X-Ascend-Force-56', + 'framed_appletalk_network' => 'Framed-AppleTalk-Network', + 'reply_message' => 'Reply-Message', + 'class' => 'Class', + 'h323_conf_id' => 'h323-conf-id', + 'quintum_h323_disconnecta' => 'Quintum-h323-disconnect-cause', + 'itk_filter_rule' => 'ITK-Filter-Rule', + 'wispr_bandwidth_max_up' => 'WISPr-Bandwidth-Max-Up', + 'usr_appletalk_network_ra' => 'USR-Appletalk-Network-Range', 'ascend_cbcp_delay' => 'Ascend-CBCP-Delay', - 'le_ipsec_deny_action' => 'LE-IPSec-Deny-Action', + 'usr_dte_ring_no_answer_l' => 'USR-DTE-Ring-No-Answer-Limit', + 'pre_acct_type' => 'Pre-Acct-Type', + 'usr_local_ip_address' => 'USR-Local-IP-Address', + 'ascend_dropped_octets' => 'Ascend-Dropped-Octets', + 'ascend_h323_dialed_time' => 'Ascend-H323-Dialed-Time', + 'cisco_email_server_addre' => 'Cisco-Email-Server-Address', + 'ascend_x25_x121_address' => 'Ascend-X25-X121-Address', + 'cvx_multicast_client' => 'CVX-Multicast-Client', + 'wispr_bandwidth_min_up' => 'WISPr-Bandwidth-Min-Up', + 'usr_at_output_filter' => 'USR-AT-Output-Filter', + 'annex_local_ip_address' => 'Annex-Local-IP-Address', + 'cisco_ip_pool_definition' => 'Cisco-IP-Pool-Definition', + 'cisco_gateway_id' => 'Cisco-Gateway-Id', + 'itk_password_prompt' => 'ITK-Password-Prompt', + 'annex_domain_name' => 'Annex-Domain-Name', + 'foundry_command_exceptio' => 'Foundry-Command-Exception-Flag', + 'ascend_preempt_limit' => 'Ascend-Preempt-Limit', + 'erx_minimum_bps' => 'ERX-Minimum-BPS', + 'aat_mcast_client' => 'AAT-MCast-Client', + 'ascend_atm_fault_managem' => 'Ascend-ATM-Fault-Management', + 'ascend_event_type' => 'Ascend-Event-Type', + 'exec_program_wait' => 'Exec-Program-Wait', + 'framed_interface_id' => 'Framed-Interface-Id', + + #NETC.NET.AU (RADIATOR?) + 'authentication_type' => 'Authentication-Type', - #NOMENT - 'nomadix_bw_down' => 'Nomadix-Bw-Down', - 'nomadix_bw_up' => 'Nomadix-Bw-Up', - 'nomadix_ip_upsell' => 'Nomadix-IP-Upsell', ); 1; diff --git a/FS/FS/radius_usergroup.pm b/FS/FS/radius_usergroup.pm index 647621d28..9bba057c9 100644 --- a/FS/FS/radius_usergroup.pm +++ b/FS/FS/radius_usergroup.pm @@ -100,6 +100,7 @@ sub check { || $self->ut_number('svcnum') || $self->ut_foreign_key('svcnum','svc_acct','svcnum') || $self->ut_text('groupname') + || $self->SUPER::check ; } diff --git a/FS/FS/router.pm b/FS/FS/router.pm new file mode 100755 index 000000000..2554ce86b --- /dev/null +++ b/FS/FS/router.pm @@ -0,0 +1,144 @@ +package FS::router; + +use strict; +use vars qw( @ISA ); +use FS::Record qw( qsearchs qsearch ); +use FS::addr_block; + +@ISA = qw( FS::Record ); + +=head1 NAME + +FS::router - Object methods for router records + +=head1 SYNOPSIS + + use FS::router; + + $record = new FS::router \%hash; + $record = new FS::router { 'column' => 'value' }; + + $error = $record->insert; + + $error = $new_record->replace($old_record); + + $error = $record->delete; + + $error = $record->check; + +=head1 DESCRIPTION + +An FS::router record describes a broadband router, such as a DSLAM or a wireless + access point. FS::router inherits from FS::Record. The following +fields are currently supported: + +=over 4 + +=item routernum - primary key + +=item routername - descriptive name for the router + +=item svcnum - svcnum of the owning FS::svc_broadband, if appropriate + +=back + +=head1 METHODS + +=over 4 + +=item new HASHREF + +Create a new record. To add the record to the database, see "insert". + +=cut + +sub table { 'router'; } + +=item insert + +Adds this record to the database. If there is an error, returns the error, +otherwise returns false. + +=item delete + +Deletes this record from the database. If there is an error, returns the +error, otherwise returns false. + +=item replace OLD_RECORD + +Replaces OLD_RECORD with this one in the database. If there is an error, +returns the error, otherwise returns false. + +=item check + +Checks all fields to make sure this is a valid record. If there is an error, +returns the error, otherwise returns false. Called by the insert and replace +methods. + +=cut + +sub check { + my $self = shift; + + my $error = + $self->ut_numbern('routernum') + || $self->ut_text('routername'); + return $error if $error; + + $self->SUPER::check; +} + +=item addr_block + +Returns a list of FS::addr_block objects (address blocks) associated +with this object. + +=cut + +sub addr_block { + my $self = shift; + return qsearch('addr_block', { routernum => $self->routernum }); +} + +=item part_svc_router + +Returns a list of FS::part_svc_router objects associated with this +object. This is unlikely to be useful for any purpose other than retrieving +the associated FS::part_svc objects. See below. + +=cut + +sub part_svc_router { + my $self = shift; + return qsearch('part_svc_router', { routernum => $self->routernum }); +} + +=item part_svc + +Returns a list of FS::part_svc objects associated with this object. + +=cut + +sub part_svc { + my $self = shift; + return map { qsearchs('part_svc', { svcpart => $_->svcpart }) } + $self->part_svc_router; +} + +=back + +=head1 VERSION + +$Id: + +=head1 BUGS + +=head1 SEE ALSO + +FS::svc_broadband, FS::router, FS::addr_block, FS::part_svc, +schema.html from the base documentation. + +=cut + +1; + diff --git a/FS/FS/session.pm b/FS/FS/session.pm index de0f2a76a..2ad594cf2 100644 --- a/FS/FS/session.pm +++ b/FS/FS/session.pm @@ -216,7 +216,7 @@ sub check { return $error if $error; return "Unknown svcnum" unless qsearchs('svc_acct', { 'svcnum' => $self->svcnum } ); - ''; + $self->SUPER::check; } =item nas_heartbeat @@ -247,7 +247,7 @@ sub svc_acct { =head1 VERSION -$Id: session.pm,v 1.7 2001-04-15 13:35:12 ivan Exp $ +$Id: session.pm,v 1.8 2003-08-05 00:20:46 khoff Exp $ =head1 BUGS diff --git a/FS/FS/svc_Common.pm b/FS/FS/svc_Common.pm index 87b6097aa..a154f3f85 100644 --- a/FS/FS/svc_Common.pm +++ b/FS/FS/svc_Common.pm @@ -2,7 +2,7 @@ package FS::svc_Common; use strict; use vars qw( @ISA $noexport_hack ); -use FS::Record qw( qsearchs fields dbh ); +use FS::Record qw( qsearch qsearchs fields dbh ); use FS::cust_svc; use FS::part_svc; use FS::queue; @@ -28,7 +28,61 @@ inherit from, i.e. FS::svc_acct. FS::svc_Common inherits from FS::Record. =over 4 -=item insert [ JOBNUM_ARRAYREF ] +=cut + +sub virtual_fields { + + # This restricts the fields based on part_svc_column and the svcpart of + # the service. There are four possible cases: + # 1. svcpart passed as part of the svc_x hash. + # 2. svcpart fetched via cust_svc based on svcnum. + # 3. No svcnum or svcpart. In this case, return ALL the fields with + # dbtable eq $self->table. + # 4. Called via "fields('svc_acct')" or something similar. In this case + # there is no $self object. + + my $self = shift; + my $svcpart; + my @vfields = $self->SUPER::virtual_fields; + + return @vfields unless (ref $self); # Case 4 + + if ($self->svcpart) { # Case 1 + $svcpart = $self->svcpart; + } elsif ( $self->svcnum + && qsearchs('cust_svc',{'svcnum'=>$self->svcnum} ) + ) { #Case 2 + $svcpart = $self->cust_svc->svcpart; + } else { # Case 3 + $svcpart = ''; + } + + if ($svcpart) { #Cases 1 and 2 + my %flags = map { $_->columnname, $_->columnflag } ( + qsearch ('part_svc_column', { svcpart => $svcpart } ) + ); + return grep { not ($flags{$_} eq 'X') } @vfields; + } else { # Case 3 + return @vfields; + } + return (); +} + +=item check + +Checks the validity of fields in this record. + +At present, this does nothing but call FS::Record::check (which, in turn, +does nothing but run virtual field checks). + +=cut + +sub check { + my $self = shift; + $self->SUPER::check; +} + +=item insert [ JOBNUM_ARRAYREF [ OBJECTS_ARRAYREF ] ] Adds this record to the database. If there is an error, returns the error, otherwise returns false. @@ -39,11 +93,16 @@ defined. An FS::cust_svc record will be created and inserted. If an arrayref is passed as parameter, the B<jobnum>s of any export jobs will be added to the array. +If an arrayref of FS::tablename objects (for example, FS::acct_snarf objects) +is passed as the optional second parameter, they will have their svcnum fields +set and will be inserted after this record, but before any exports are run. + =cut sub insert { my $self = shift; local $FS::queue::jobnums = shift if @_; + my $objects = scalar(@_) ? shift : []; my $error; local $SIG{HUP} = 'IGNORE'; @@ -61,10 +120,12 @@ sub insert { return $error if $error; my $svcnum = $self->svcnum; - my $cust_svc; - unless ( $svcnum ) { + my $cust_svc = $svcnum ? qsearchs('cust_svc',{'svcnum'=>$self->svcnum}) : ''; + #unless ( $svcnum ) { + if ( !$svcnum or !$cust_svc ) { $cust_svc = new FS::cust_svc ( { #hua?# 'svcnum' => $svcnum, + 'svcnum' => $self->svcnum, 'pkgnum' => $self->pkgnum, 'svcpart' => $self->svcpart, } ); @@ -75,7 +136,7 @@ sub insert { } $svcnum = $self->svcnum($cust_svc->svcnum); } else { - $cust_svc = qsearchs('cust_svc',{'svcnum'=>$self->svcnum}); + #$cust_svc = qsearchs('cust_svc',{'svcnum'=>$self->svcnum}); unless ( $cust_svc ) { $dbh->rollback if $oldAutoCommit; return "no cust_svc record found for svcnum ". $self->svcnum; @@ -90,6 +151,15 @@ sub insert { return $error; } + foreach my $object ( @$objects ) { + $object->svcnum($self->svcnum); + $error = $object->insert; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + } + #new-style exports! unless ( $noexport_hack ) { foreach my $part_export ( $self->cust_svc->part_svc->part_export ) { @@ -242,7 +312,7 @@ sub setx { #get part_svc my $svcpart; - if ( $self->svcnum ) { + if ( $self->svcnum && qsearchs('cust_svc', {'svcnum'=>$self->svcnum}) ) { my $cust_svc = $self->cust_svc; return "Unknown svcnum" unless $cust_svc; $svcpart = $cust_svc->svcpart; @@ -254,7 +324,7 @@ sub setx { #set default/fixed/whatever fields from part_svc my $table = $self->table; - foreach my $field ( grep { $_ ne 'svcnum' } fields($table) ) { + foreach my $field ( grep { $_ ne 'svcnum' } $self->fields ) { my $part_svc_column = $part_svc->part_svc_column($field); if ( $part_svc_column->columnflag eq $x ) { $self->setfield( $field, $part_svc_column->columnvalue ); @@ -360,11 +430,31 @@ methods. Called by the cancel method of FS::cust_pkg (see L<FS::cust_pkg>). sub cancel { ''; } -=back +=item clone_suspended + +Constructor used by FS::part_export::_export_suspend fallback. Stub returning +same object for svc_ classes which don't implement a suspension fallback +(everything except svc_acct at the moment). Document better. + +=cut + +sub clone_suspended { + shift; +} -=head1 VERSION +=item clone_kludge_unsuspend -$Id: svc_Common.pm,v 1.12 2002-06-14 11:22:53 ivan Exp $ +Constructor used by FS::part_export::_export_unsuspend fallback. Stub returning +same object for svc_ classes which don't implement a suspension fallback +(everything except svc_acct at the moment). Document better. + +=cut + +sub clone_kludge_unsuspend { + shift; +} + +=back =head1 BUGS diff --git a/FS/FS/svc_acct.pm b/FS/FS/svc_acct.pm index c95df94cf..32d87202e 100644 --- a/FS/FS/svc_acct.pm +++ b/FS/FS/svc_acct.pm @@ -1,28 +1,26 @@ package FS::svc_acct; use strict; -use vars qw( @ISA $noexport_hack $conf +use vars qw( @ISA $DEBUG $me $conf $dir_prefix @shells $usernamemin $usernamemax $passwordmin $passwordmax $username_ampersand $username_letter $username_letterfirst $username_noperiod $username_nounderscore $username_nodash $username_uppercase - $mydomain $welcome_template $welcome_from $welcome_subject $welcome_mimetype $smtpmachine + $radius_password $radius_ip $dirhash @saltset @pw_set ); use Carp; use Fcntl qw(:flock); use FS::UID qw( datasrc ); use FS::Conf; -use FS::Record qw( qsearch qsearchs fields dbh ); +use FS::Record qw( qsearch qsearchs fields dbh dbdef ); use FS::svc_Common; -use Net::SSH; use FS::cust_svc; use FS::part_svc; use FS::svc_acct_pop; -use FS::svc_acct_sm; use FS::cust_main_invoice; use FS::svc_domain; use FS::raddb; @@ -34,6 +32,9 @@ use FS::Msgcat qw(gettext); @ISA = qw( FS::svc_Common ); +$DEBUG = 0; +$me = '[FS::svc_acct]'; + #ask FS::UID to run this stuff for us later $FS::UID::callback{'FS::svc_acct'} = sub { $conf = new FS::Conf; @@ -50,7 +51,6 @@ $FS::UID::callback{'FS::svc_acct'} = sub { $username_nodash = $conf->exists('username-nodash'); $username_uppercase = $conf->exists('username-uppercase'); $username_ampersand = $conf->exists('username-ampersand'); - $mydomain = $conf->config('domain'); $dirhash = $conf->config('dirhash') || 0; if ( $conf->exists('welcome_email') ) { $welcome_template = new Text::Template ( @@ -62,8 +62,13 @@ $FS::UID::callback{'FS::svc_acct'} = sub { $welcome_mimetype = $conf->config('welcome_email-mimetype') || 'text/plain'; } else { $welcome_template = ''; + $welcome_from = ''; + $welcome_subject = ''; + $welcome_mimetype = ''; } $smtpmachine = $conf->config('smtpmachine'); + $radius_password = $conf->config('radius-password') || 'Password'; + $radius_ip = $conf->config('radius-ip') || 'Framed-IP-Address'; }; @saltset = ( 'a'..'z' , 'A'..'Z' , '0'..'9' , '.' , '/' ); @@ -183,9 +188,15 @@ The additional field I<usergroup> can optionally be defined; if so it should contain an arrayref of group names. See L<FS::radius_usergroup>. (used in sqlradius export only) +The additional field I<child_objects> can optionally be defined; if so it +should contain an arrayref of FS::tablename objects. They will have their +svcnum fields set and will be inserted after this record, but before any +exports are run. + (TODOC: L<FS::queue> and L<freeside-queued>) -(TODOC: new exports! $noexport_hack) +(TODOC: new exports!) + =cut @@ -215,7 +226,7 @@ sub insert { # 'domsvc' => $self->domsvc, # } ); - if ( $self->svcnum ) { + if ( $self->svcnum && qsearchs('cust_svc',{'svcnum'=>$self->svcnum}) ) { my $cust_svc = qsearchs('cust_svc',{'svcnum'=>$self->svcnum}); unless ( $cust_svc ) { $dbh->rollback if $oldAutoCommit; @@ -246,7 +257,8 @@ sub insert { if ( @dup_user || @dup_userdomain || @dup_uid ) { my $exports = FS::part_export::export_info('svc_acct'); - my( %conflict_user_svcpart, %conflict_userdomain_svcpart ); + my %conflict_user_svcpart; + my %conflict_userdomain_svcpart = ( $self->svcpart => 'SELF', ); foreach my $part_export ( $part_svc->part_export ) { @@ -264,7 +276,11 @@ sub insert { # qsearch('export_svc', { 'exportnum' => $part_export->exportnum }); #} - my $nodomain = $exports->{$part_export->exporttype}{'nodomain'}; + #my $nodomain = $exports->{$part_export->exporttype}{'nodomain'}; + #silly kludge to avoid uninitialized value errors + my $nodomain = exists( $exports->{$part_export->exporttype}{'nodomain'} ) + ? $exports->{$part_export->exporttype}{'nodomain'} + : ''; if ( $nodomain =~ /^Y/i ) { $conflict_user_svcpart{$_} = $part_export->exportnum foreach @svcparts; @@ -309,7 +325,7 @@ sub insert { #see? i told you it was more complicated my @jobnums; - $error = $self->SUPER::insert(\@jobnums); + $error = $self->SUPER::insert(\@jobnums, $self->child_objects || [] ); if ( $error ) { $dbh->rollback if $oldAutoCommit; return $error; @@ -340,47 +356,58 @@ sub insert { return "queueing job (transaction rolled back): $error"; } - #welcome email my $cust_pkg = $self->cust_svc->cust_pkg; - my( $cust_main, $to ) = ( '', '' ); - if ( $welcome_template && $cust_pkg ) { + + if ( $cust_pkg ) { my $cust_main = $cust_pkg->cust_main; - my $to = join(', ', grep { $_ ne 'POST' } $cust_main->invoicing_list ); - if ( $to ) { - my $wqueue = new FS::queue { - 'svcnum' => $self->svcnum, - 'job' => 'FS::svc_acct::send_email' - }; - warn "attempting to queue email to $to"; - my $error = $wqueue->insert( - 'to' => $to, - 'from' => $welcome_from, - 'subject' => $welcome_subject, - 'mimetype' => $welcome_mimetype, - 'body' => $welcome_template->fill_in( HASH => { - 'username' => $self->username, - 'password' => $self->_password, - 'first' => $cust_main->first, - 'last' => $cust_main->getfield('last'), - 'pkg' => $cust_pkg->part_pkg->pkg, - } ), - ); - if ( $error ) { - $dbh->rollback if $oldAutoCommit; - return "queuing welcome email: $error"; - } - - foreach my $jobnum ( @jobnums ) { - my $error = $wqueue->depend_insert($jobnum); + + if ( $conf->exists('emailinvoiceauto') ) { + my @invoicing_list = $cust_main->invoicing_list; + push @invoicing_list, $self->email; + $cust_main->invoicing_list(\@invoicing_list); + } + + #welcome email + my $to = ''; + if ( $welcome_template && $cust_pkg ) { + my $to = join(', ', grep { $_ ne 'POST' } $cust_main->invoicing_list ); + if ( $to ) { + my $wqueue = new FS::queue { + 'svcnum' => $self->svcnum, + 'job' => 'FS::svc_acct::send_email' + }; + my $error = $wqueue->insert( + 'to' => $to, + 'from' => $welcome_from, + 'subject' => $welcome_subject, + 'mimetype' => $welcome_mimetype, + 'body' => $welcome_template->fill_in( HASH => { + 'custnum' => $self->custnum, + 'username' => $self->username, + 'password' => $self->_password, + 'first' => $cust_main->first, + 'last' => $cust_main->getfield('last'), + 'pkg' => $cust_pkg->part_pkg->pkg, + } ), + ); if ( $error ) { $dbh->rollback if $oldAutoCommit; - return "queuing welcome email job dependancy: $error"; + return "error queuing welcome email: $error"; } + + foreach my $jobnum ( @jobnums ) { + my $error = $wqueue->depend_insert($jobnum); + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "error queuing welcome email job dependancy: $error"; + } + } + } } - - } + + } # if ( $cust_pkg ) $dbh->commit or die $dbh->errstr if $oldAutoCommit; ''; #no error @@ -393,17 +420,14 @@ error, otherwise returns false. The corresponding FS::cust_svc record will be deleted as well. -(TODOC: new exports! $noexport_hack) +(TODOC: new exports!) =cut sub delete { my $self = shift; - if ( defined( $FS::Record::dbdef->table('svc_acct_sm') ) ) { - return "Can't delete an account which has (svc_acct_sm) mail aliases!" - if $self->uid && qsearch( 'svc_acct_sm', { 'domuid' => $self->uid } ); - } + return "can't delete system account" if $self->_check_system; return "Can't delete an account which is a (svc_forward) source!" if qsearch( 'svc_forward', { 'srcsvc' => $self->svcnum } ); @@ -491,6 +515,9 @@ sqlradius export only) sub replace { my ( $new, $old ) = ( shift, shift ); my $error; + warn "$me replacing $old with $new\n" if $DEBUG; + + return "can't modify system account" if $old->_check_system; return "Username in use" if $old->username ne $new->username && @@ -517,7 +544,13 @@ sub replace { local $FS::UID::AutoCommit = 0; my $dbh = dbh; + # redundant, but so $new->usergroup gets set + $error = $new->check; + return $error if $error; + $old->usergroup( [ $old->radius_groups ] ); + warn "old groups: ". join(' ',@{$old->usergroup}). "\n" if $DEBUG; + warn "new groups: ". join(' ',@{$new->usergroup}). "\n" if $DEBUG; if ( $new->usergroup ) { #(sorta) false laziness with FS::part_export::sqlradius::_export_replace my @newgroups = @{$new->usergroup}; @@ -557,26 +590,27 @@ sub replace { return $error if $error; } - #false laziness with sub insert (and cust_main) - my $queue = new FS::queue { - 'svcnum' => $new->svcnum, - 'job' => 'FS::svc_acct::append_fuzzyfiles' - }; - $error = $queue->insert($new->username); - if ( $error ) { - $dbh->rollback if $oldAutoCommit; - return "queueing job (transaction rolled back): $error"; + if ( $new->username ne $old->username ) { + #false laziness with sub insert (and cust_main) + my $queue = new FS::queue { + 'svcnum' => $new->svcnum, + 'job' => 'FS::svc_acct::append_fuzzyfiles' + }; + $error = $queue->insert($new->username); + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "queueing job (transaction rolled back): $error"; + } } - $dbh->commit or die $dbh->errstr if $oldAutoCommit; ''; #no error } =item suspend -Suspends this account by prefixing *SUSPENDED* to the password. If there is an -error, returns the error, otherwise returns false. +Suspends this account by calling export-specific suspend hooks. If there is +an error, returns the error, otherwise returns false. Called by the suspend method of FS::cust_pkg (see L<FS::cust_pkg>). @@ -584,23 +618,14 @@ Called by the suspend method of FS::cust_pkg (see L<FS::cust_pkg>). sub suspend { my $self = shift; - my %hash = $self->hash; - unless ( $hash{_password} =~ /^\*SUSPENDED\* / - || $hash{_password} eq '*' - ) { - $hash{_password} = '*SUSPENDED* '.$hash{_password}; - my $new = new FS::svc_acct ( \%hash ); - my $error = $new->replace($self); - return $error if $error; - } - + return "can't suspend system account" if $self->_check_system; $self->SUPER::suspend; } =item unsuspend -Unsuspends this account by removing *SUSPENDED* from the password. If there is -an error, returns the error, otherwise returns false. +Unsuspends this account by by calling export-specific suspend hooks. If there +is an error, returns the error, otherwise returns false. Called by the unsuspend method of FS::cust_pkg (see L<FS::cust_pkg>). @@ -650,7 +675,8 @@ sub check { } my $error = $self->ut_numbern('svcnum') - || $self->ut_number('domsvc') + #|| $self->ut_number('domsvc') + || $self->ut_foreign_key('domsvc', 'svc_domain', 'svcnum' ) || $self->ut_textn('sec_phrase') ; return $error if $error; @@ -705,15 +731,9 @@ sub check { && $recref->{username} ne 'root' && $recref->{username} ne 'toor'; -# $error = $self->ut_textn('finger'); -# return $error if $error; - $self->getfield('finger') =~ - /^([\w \t\!\@\#\$\%\&\(\)\-\+\;\:\'\"\,\.\?\/\*\<\>]*)$/ - or return "Illegal finger: ". $self->getfield('finger'); - $self->setfield('finger', $1); $recref->{dir} =~ /^([\/\w\-\.\&]*)$/ - or return "Illegal directory"; + or return "Illegal directory: ". $recref->{dir}; $recref->{dir} = $1; return "Illegal directory" if $recref->{dir} =~ /(^|\/)\.+(\/|$)/; #no .. component @@ -745,29 +765,34 @@ sub check { $recref->{shell} = '/bin/sync'; } - $recref->{quota} =~ /^(\d*)$/ or return "Illegal quota (unimplemented)"; - $recref->{quota} = $1; - } else { $recref->{gid} ne '' ? return "Can't have gid without uid" : ( $recref->{gid}='' ); - $recref->{finger} ne '' ? - return "Can't have finger-name without uid" : ( $recref->{finger}='' ); $recref->{dir} ne '' ? return "Can't have directory without uid" : ( $recref->{dir}='' ); $recref->{shell} ne '' ? return "Can't have shell without uid" : ( $recref->{shell}='' ); - $recref->{quota} ne '' ? - return "Can't have quota without uid" : ( $recref->{quota}='' ); } + # $error = $self->ut_textn('finger'); + # return $error if $error; + $self->getfield('finger') =~ + /^([\w \t\!\@\#\$\%\&\(\)\-\+\;\'\"\,\.\?\/\*\<\>]*)$/ + or return "Illegal finger: ". $self->getfield('finger'); + $self->setfield('finger', $1); + + $recref->{quota} =~ /^(\w*)$/ or return "Illegal quota"; + $recref->{quota} = $1; + unless ( $part_svc->part_svc_column('slipip')->columnflag eq 'F' ) { - unless ( $recref->{slipip} eq '0e0' ) { + if ( $recref->{slipip} eq '' ) { + $recref->{slipip} = ''; + } elsif ( $recref->{slipip} eq '0e0' ) { + $recref->{slipip} = '0e0'; + } else { $recref->{slipip} =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/ - or return "Illegal slipip". $self->slipip; + or return "Illegal slipip: ". $self->slipip; $recref->{slipip} = $1; - } else { - $recref->{slipip} = '0e0'; } } @@ -791,10 +816,12 @@ sub check { #$recref->{password} = $1. # crypt($3,$saltset[int(rand(64))].$saltset[int(rand(64))] #; - } elsif ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([\w\.\/\$]{13,34})$/ ) { + } elsif ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([\w\.\/\$\;\+]{13,60})$/ ) { $recref->{_password} = $1.$3; } elsif ( $recref->{_password} eq '*' ) { $recref->{_password} = '*'; + } elsif ( $recref->{_password} eq '!' ) { + $recref->{_password} = '!'; } elsif ( $recref->{_password} eq '!!' ) { $recref->{_password} = '!!'; } else { @@ -804,7 +831,18 @@ sub check { ": ". $recref->{_password}; } - ''; #no error + $self->SUPER::check; +} + +=item _check_system + +=cut + +sub _check_system { + my $self = shift; + scalar( grep { $self->username eq $_ || $self->email eq $_ } + $conf->config('system_usernames') + ); } =item radius @@ -839,7 +877,7 @@ sub radius_reply { ( $FS::raddb::attrib{lc($attrib)}, $self->getfield($column) ); } grep { /^radius_/ && $self->getfield($_) } fields( $self->table ); if ( $self->slipip && $self->slipip ne '0e0' ) { - $reply{'Framed-IP-Address'} = $self->slipip; + $reply{$radius_ip} = $self->slipip; } %reply; } @@ -857,7 +895,9 @@ expected to change in the future. sub radius_check { my $self = shift; - ( 'Password' => $self->_password, + my $password = $self->_password; + my $pw_attrib = length($password) <= 12 ? $radius_password : 'Crypt-Password'; + ( $pw_attrib => $password, map { /^(rc_(.*))$/; my($column, $attrib) = ($1, $2); @@ -875,14 +915,10 @@ Returns the domain associated with this account. sub domain { my $self = shift; - if ( $self->domsvc ) { - #$self->svc_domain->domain; - my $svc_domain = $self->svc_domain - or die "no svc_domain.svcnum for svc_acct.domsvc ". $self->domsvc; - $svc_domain->domain; - } else { - $mydomain or die "svc_acct.domsvc is null and no legacy domain config file"; - } + die "svc_acct.domsvc is null for svcnum ". $self->svcnum unless $self->domsvc; + my $svc_domain = $self->svc_domain + or die "no svc_domain.svcnum for svc_acct.domsvc ". $self->domsvc; + $svc_domain->domain; } =item svc_domain @@ -903,6 +939,8 @@ sub svc_domain { Returns the FS::cust_svc record for this account (see L<FS::cust_svc>). +=cut + sub cust_svc { my $self = shift; qsearchs( 'cust_svc', { 'svcnum' => $self->svcnum } ); @@ -919,10 +957,26 @@ sub email { $self->username. '@'. $self->domain; } +=item acct_snarf + +Returns an array of FS::acct_snarf records associated with the account. +If the acct_snarf table does not exist or there are no associated records, +an empty list is returned + +=cut + +sub acct_snarf { + my $self = shift; + return () unless dbdef->table('acct_snarf'); + eval "use FS::acct_snarf;"; + die $@ if $@; + qsearch('acct_snarf', { 'svcnum' => $self->svcnum } ); +} + =item seconds_since TIMESTAMP -Returns the number of seconds this account has been online since TIMESTAMP. -See L<FS::session> +Returns the number of seconds this account has been online since TIMESTAMP, +according to the session monitor (see L<FS::Session>). TIMESTAMP is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion functions. @@ -935,6 +989,60 @@ sub seconds_since { $self->cust_svc->seconds_since(@_); } +=item seconds_since_sqlradacct TIMESTAMP_START TIMESTAMP_END + +Returns the numbers of seconds this account has been online between +TIMESTAMP_START (inclusive) and TIMESTAMP_END (exclusive), according to an +external SQL radacct table, specified via sqlradius export. Sessions which +started in the specified range but are still open are counted from session +start to the end of the range (unless they are over 1 day old, in which case +they are presumed missing their stop record and not counted). Also, sessions +which end in the range but started earlier are counted from the start of the +range to session end. Finally, sessions which start before the range but end +after are counted for the entire range. + +TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see +L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion +functions. + +=cut + +#note: POD here, implementation in FS::cust_svc +sub seconds_since_sqlradacct { + my $self = shift; + $self->cust_svc->seconds_since_sqlradacct(@_); +} + +=item attribute_since_sqlradacct TIMESTAMP_START TIMESTAMP_END ATTRIBUTE + +Returns the sum of the given attribute for all accounts (see L<FS::svc_acct>) +in this package for sessions ending between TIMESTAMP_START (inclusive) and +TIMESTAMP_END (exclusive). + +TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see +L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion +functions. + +=cut + +#note: POD here, implementation in FS::cust_svc +sub attribute_since_sqlradacct { + my $self = shift; + $self->cust_svc->attribute_since_sqlradacct(@_); +} + +=item get_session_history_sqlradacct TIMESTAMP_START TIMESTAMP_END + +Returns an array of hash references of this customers login history for the +given time range. (document this better) + +=cut + +sub get_session_history_sqlradacct { + my $self = shift; + $self->cust_svc->get_session_history_sqlradacct(@_); +} + =item radius_groups Returns all RADIUS groups for this account (see L<FS::radius_usergroup>). @@ -953,6 +1061,34 @@ sub radius_groups { } } +=item clone_suspended + +Constructor used by FS::part_export::_export_suspend fallback. Document +better. + +=cut + +sub clone_suspended { + my $self = shift; + my %hash = $self->hash; + $hash{_password} = join('',map($pw_set[ int(rand $#pw_set) ], (0..7) ) ); + new FS::svc_acct \%hash; +} + +=item clone_kludge_unsuspend + +Constructor used by FS::part_export::_export_unsuspend fallback. Document +better. + +=cut + +sub clone_kludge_unsuspend { + my $self = shift; + my %hash = $self->hash; + $hash{_password} = ''; + new FS::svc_acct \%hash; +} + =back =head1 SUBROUTINES @@ -961,36 +1097,28 @@ sub radius_groups { =item send_email +This is the FS::svc_acct job-queue-able version. It still uses +FS::Misc::send_email under-the-hood. + =cut sub send_email { my %opt = @_; - use Date::Format; - use Mail::Internet 1.44; - use Mail::Header; + eval "use FS::Misc qw(send_email)"; + die $@ if $@; $opt{mimetype} ||= 'text/plain'; $opt{mimetype} .= '; charset="iso-8859-1"' unless $opt{mimetype} =~ /charset/; - $ENV{MAILADDRESS} = $opt{from}; - my $header = new Mail::Header ( [ - "From: $opt{from}", - "To: $opt{to}", - "Sender: $opt{from}", - "Reply-To: $opt{from}", - "Date: ". time2str("%a, %d %b %Y %X %z", time), - "Subject: $opt{subject}", - "Content-Type: $opt{mimetype}", - ] ); - my $message = new Mail::Internet ( - 'Header' => $header, - 'Body' => [ map "$_\n", split("\n", $opt{body}) ], + my $error = send_email( + 'from' => $opt{from}, + 'to' => $opt{to}, + 'subject' => $opt{subject}, + 'content-type' => $opt{mimetype}, + 'body' => [ map "$_\n", split("\n", $opt{body}) ], ); - $!=0; - $message->smtpsend( Host => $smtpmachine ) - or $message->smtpsend( Host => $smtpmachine, Debug => 1 ) - or die "can't send email to $opt{to} via $smtpmachine with SMTP: $!"; + die $error if $error; } =item check_and_rebuild_fuzzyfiles @@ -1141,7 +1269,7 @@ probably live somewhere else... L<FS::svc_Common>, edit/part_svc.cgi from an installed web interface, export.html from the base documentation, L<FS::Record>, L<FS::Conf>, L<FS::cust_svc>, L<FS::part_svc>, L<FS::cust_pkg>, L<FS::queue>, -L<freeside-queued>), L<Net::SSH>, L<ssh>, L<FS::svc_acct_pop>, +L<freeside-queued>), L<FS::svc_acct_pop>, schema.html from the base documentation. =cut diff --git a/FS/FS/svc_acct_pop.pm b/FS/FS/svc_acct_pop.pm index 3c9ea0130..f98f91a4f 100644 --- a/FS/FS/svc_acct_pop.pm +++ b/FS/FS/svc_acct_pop.pm @@ -93,6 +93,7 @@ sub check { or $self->ut_number('ac') or $self->ut_number('exch') or $self->ut_numbern('loc') + or $self->SUPER::check ; } @@ -142,8 +143,7 @@ sub popselector { function popstate_changed(what) { state = what.options[what.selectedIndex].text; - for (var i = what.form.popnum.length;i > 0;i--) - what.form.popnum.options[i] = null; + what.form.popnum.options.length = 0 what.form.popnum.options[0] = new Option("", "", false, true); END @@ -167,7 +167,13 @@ END $text .= '</SELECT>'; #callback? return 3 html pieces? #'</TD><TD>'; $text .= qq!<SELECT NAME="popnum" SIZE=1><OPTION> !; - foreach my $pop ( @svc_acct_pop ) { + my @initial_select; + if ( scalar(@svc_acct_pop) > 100 ) { + @initial_select = qsearchs( 'svc_acct_pop', { 'popnum' => $popnum } ); + } else { + @initial_select = @svc_acct_pop; + } + foreach my $pop ( @initial_select ) { $text .= qq!<OPTION VALUE="!. $pop->popnum. '"'. ( ( $popnum && $pop->popnum == $popnum ) ? ' SELECTED' : '' ). ">". $pop->text; @@ -182,7 +188,7 @@ END =head1 VERSION -$Id: svc_acct_pop.pm,v 1.7 2002-04-10 13:42:48 ivan Exp $ +$Id: svc_acct_pop.pm,v 1.10 2003-08-05 00:20:47 khoff Exp $ =head1 BUGS diff --git a/FS/FS/svc_acct_sm.pm b/FS/FS/svc_acct_sm.pm deleted file mode 100644 index c92f1421f..000000000 --- a/FS/FS/svc_acct_sm.pm +++ /dev/null @@ -1,260 +0,0 @@ -package FS::svc_acct_sm; - -use strict; -use vars qw( @ISA $nossh_hack $conf $shellmachine @qmailmachines ); -use FS::Record qw( fields qsearch qsearchs ); -use FS::svc_Common; -use FS::cust_svc; -use Net::SSH qw(ssh); -use FS::Conf; -use FS::svc_acct; -use FS::svc_domain; - -@ISA = qw( FS::svc_Common ); - -#ask FS::UID to run this stuff for us later -#$FS::UID::callback{'FS::svc_acct_sm'} = sub { -# $conf = new FS::Conf; -# $shellmachine = $conf->exists('qmailmachines') -# ? $conf->config('shellmachine') -# : ''; -#}; - -=head1 NAME - -FS::svc_acct_sm - Object methods for svc_acct_sm records - -=head1 SYNOPSIS - - use FS::svc_acct_sm; - - $record = new FS::svc_acct_sm \%hash; - $record = new FS::svc_acct_sm { 'column' => 'value' }; - - $error = $record->insert; - - $error = $new_record->replace($old_record); - - $error = $record->delete; - - $error = $record->check; - - $error = $record->suspend; - - $error = $record->unsuspend; - - $error = $record->cancel; - -=head1 WARNING - -FS::svc_acct_sm is B<depreciated>. This class is only included for migration -purposes. See L<FS::svc_forward>. - -=head1 DESCRIPTION - -An FS::svc_acct_sm object represents a virtual mail alias. FS::svc_acct_sm -inherits from FS::Record. The following fields are currently supported: - -=over 4 - -=item svcnum - primary key (assigned automatcially for new accounts) - -=item domsvc - svcnum of the virtual domain (see L<FS::svc_domain>) - -=item domuid - uid of the target account (see L<FS::svc_acct>) - -=item domuser - virtual username - -=back - -=head1 METHODS - -=over 4 - -=item new HASHREF - -Creates a new virtual mail alias. To add the virtual mail alias to the -database, see L<"insert">. - -=cut - -sub table { 'svc_acct_sm'; } - -=item insert - -Adds this virtual mail alias to the database. If there is an error, returns -the error, otherwise returns false. - -The additional fields pkgnum and svcpart (see L<FS::cust_svc>) should be -defined. An FS::cust_svc record will be created and inserted. - - #If the configuration values (see L<FS::Conf>) shellmachine and qmailmachines - #exist, and domuser is `*' (meaning a catch-all mailbox), the command: - # - # [ -e $dir/.qmail-$qdomain-default ] || { - # touch $dir/.qmail-$qdomain-default; - # chown $uid:$gid $dir/.qmail-$qdomain-default; - # } - # - #is executed on shellmachine via ssh (see L<dot-qmail/"EXTENSION ADDRESSES">). - #This behaviour can be surpressed by setting $FS::svc_acct_sm::nossh_hack true. - -=cut - -sub insert { - my $self = shift; - my $error; - - local $SIG{HUP} = 'IGNORE'; - local $SIG{INT} = 'IGNORE'; - local $SIG{QUIT} = 'IGNORE'; - local $SIG{TERM} = 'IGNORE'; - local $SIG{TSTP} = 'IGNORE'; - local $SIG{PIPE} = 'IGNORE'; - - $error=$self->check; - return $error if $error; - - return "Domain username (domuser) in use for this domain (domsvc)" - if qsearchs('svc_acct_sm',{ 'domuser'=> $self->domuser, - 'domsvc' => $self->domsvc, - } ); - - return "First domain username (domuser) for domain (domsvc) must be " . - qq='*' (catch-all)!= - if $self->domuser ne '*' - && ! qsearch('svc_acct_sm',{ 'domsvc' => $self->domsvc } ) - && ! $conf->exists('maildisablecatchall'); - - $error = $self->SUPER::insert; - return $error if $error; - - #my $svc_domain = qsearchs( 'svc_domain', { 'svcnum' => $self->domsvc } ); - #my $svc_acct = qsearchs( 'svc_acct', { 'uid' => $self->domuid } ); - #my ( $uid, $gid, $dir, $domain ) = ( - # $svc_acct->uid, - # $svc_acct->gid, - # $svc_acct->dir, - # $svc_domain->domain, - #); - #my $qdomain = $domain; - #$qdomain =~ s/\./:/g; #see manpage for 'dot-qmail': EXTENSION ADDRESSES - #ssh("root\@$shellmachine","[ -e $dir/.qmail-$qdomain-default ] || { touch $dir/.qmail-$qdomain-default; chown $uid:$gid $dir/.qmail-$qdomain-default; }") - # if ( ! $nossh_hack && $shellmachine && $dir && $self->domuser eq '*' ); - - ''; #no error - -} - -=item delete - -Deletes this virtual mail alias from the database. If there is an error, -returns the error, otherwise returns false. - -The corresponding FS::cust_svc record will be deleted as well. - -=item replace OLD_RECORD - -Replaces OLD_RECORD with this one in the database. If there is an error, -returns the error, otherwise returns false. - -=cut - -sub replace { - my ( $new, $old ) = ( shift, shift ); - my $error; - - return "Domain username (domuser) in use for this domain (domsvc)" - if ( $old->domuser ne $new->domuser - || $old->domsvc != $new->domsvc - ) && qsearchs('svc_acct_sm',{ - 'domuser'=> $new->domuser, - 'domsvc' => $new->domsvc, - } ) - ; - - $new->SUPER::replace($old); - -} - -=item suspend - -Just returns false (no error) for now. - -Called by the suspend method of FS::cust_pkg (see L<FS::cust_pkg>). - -=item unsuspend - -Just returns false (no error) for now. - -Called by the unsuspend method of FS::cust_pkg (see L<FS::cust_pkg>). - -=item cancel - -Just returns false (no error) for now. - -Called by the cancel method of FS::cust_pkg (see L<FS::cust_pkg>). - -=item check - -Checks all fields to make sure this is a valid virtual mail alias. If there is -an error, returns the error, otherwise returns false. Called by the insert and -replace methods. - -Sets any fixed values; see L<FS::part_svc>. - -=cut - -sub check { - my $self = shift; - my $error; - - my $x = $self->setfixed; - return $x unless ref($x); - #my $part_svc = $x; - - my($recref) = $self->hashref; - - $recref->{domuser} =~ /^(\*|[a-z0-9_\-]{2,32})$/ - or return "Illegal domain username (domuser)"; - $recref->{domuser} = $1; - - $recref->{domsvc} =~ /^(\d+)$/ or return "Illegal domsvc"; - $recref->{domsvc} = $1; - my($svc_domain); - return "Unknown domsvc" unless - $svc_domain=qsearchs('svc_domain',{'svcnum'=> $recref->{domsvc} } ); - - $recref->{domuid} =~ /^(\d+)$/ or return "Illegal uid"; - $recref->{domuid} = $1; - my($svc_acct); - return "Unknown uid" unless - $svc_acct=qsearchs('svc_acct',{'uid'=> $recref->{domuid} } ); - - ''; #no error -} - -=back - -=head1 VERSION - -$Id: svc_acct_sm.pm,v 1.5 2001-09-06 20:41:59 ivan Exp $ - -=head1 BUGS - -The remote commands should be configurable. - -The $recref stuff in sub check should be cleaned up. - -=head1 SEE ALSO - -L<FS::svc_forward> - -L<FS::Record>, L<FS::Conf>, L<FS::cust_svc>, L<FS::part_svc>, L<FS::cust_pkg>, -L<FS::svc_acct>, L<FS::svc_domain>, L<Net::SSH>, L<ssh>, L<dot-qmail>, -schema.html from the base documentation. - -=cut - -1; - diff --git a/FS/FS/svc_broadband.pm b/FS/FS/svc_broadband.pm new file mode 100755 index 000000000..ec915327b --- /dev/null +++ b/FS/FS/svc_broadband.pm @@ -0,0 +1,235 @@ +package FS::svc_broadband; + +use strict; +use vars qw(@ISA $conf); +use FS::Record qw( qsearchs qsearch dbh ); +use FS::svc_Common; +use FS::cust_svc; +use FS::addr_block; +use NetAddr::IP; + +@ISA = qw( FS::svc_Common ); + +$FS::UID::callback{'FS::svc_broadband'} = sub { + $conf = new FS::Conf; +}; + +=head1 NAME + +FS::svc_broadband - Object methods for svc_broadband records + +=head1 SYNOPSIS + + use FS::svc_broadband; + + $record = new FS::svc_broadband \%hash; + $record = new FS::svc_broadband { 'column' => 'value' }; + + $error = $record->insert; + + $error = $new_record->replace($old_record); + + $error = $record->delete; + + $error = $record->check; + + $error = $record->suspend; + + $error = $record->unsuspend; + + $error = $record->cancel; + +=head1 DESCRIPTION + +An FS::svc_broadband object represents a 'broadband' Internet connection, such +as a DSL, cable modem, or fixed wireless link. These services are assumed to +have the following properties: + +FS::svc_broadband inherits from FS::svc_Common. The following fields are +currently supported: + +=over 4 + +=item svcnum - primary key + +=item blocknum - see FS::addr_block + +=item +speed_up - maximum upload speed, in bits per second. If set to zero, upload +speed will be unlimited. Exports that do traffic shaping should handle this +correctly, and not blindly set the upload speed to zero and kill the customer's +connection. + +=item +speed_down - maximum download speed, as above + +=item ip_addr - the customer's IP address. If the customer needs more than one +IP address, set this to the address of the customer's router. As a result, the +customer's router will have the same address for both its internal and external +interfaces thus saving address space. This has been found to work on most NAT +routers available. + +=back + +=head1 METHODS + +=over 4 + +=item new HASHREF + +Creates a new svc_broadband. To add the record to the database, see +"insert". + +Note that this stores the hash reference, not a distinct copy of the hash it +points to. You can ask the object for a copy with the I<hash> method. + +=cut + +sub table { 'svc_broadband'; } + +=item insert + +Adds this record to the database. If there is an error, returns the error, +otherwise returns false. + +The additional fields pkgnum and svcpart (see FS::cust_svc) should be +defined. An FS::cust_svc record will be created and inserted. + +=cut + +# Standard FS::svc_Common::insert + +=item delete + +Delete this record from the database. + +=cut + +# Standard FS::svc_Common::delete + +=item replace OLD_RECORD + +Replaces the OLD_RECORD with this one in the database. If there is an error, +returns the error, otherwise returns false. + +=cut + +# Standard FS::svc_Common::replace + +=item suspend + +Called by the suspend method of FS::cust_pkg (see FS::cust_pkg). + +=item unsuspend + +Called by the unsuspend method of FS::cust_pkg (see FS::cust_pkg). + +=item cancel + +Called by the cancel method of FS::cust_pkg (see FS::cust_pkg). + +=item check + +Checks all fields to make sure this is a valid broadband service. If there is +an error, returns the error, otherwise returns false. Called by the insert +and replace methods. + +=cut + +sub check { + my $self = shift; + my $x = $self->setfixed; + + return $x unless ref($x); + + my $error = + $self->ut_numbern('svcnum') + || $self->ut_foreign_key('blocknum', 'addr_block', 'blocknum') + || $self->ut_number('speed_up') + || $self->ut_number('speed_down') + || $self->ut_ipn('ip_addr') + ; + return $error if $error; + + if($self->speed_up < 0) { return 'speed_up must be positive'; } + if($self->speed_down < 0) { return 'speed_down must be positive'; } + + if (not($self->ip_addr) or $self->ip_addr eq '0.0.0.0') { + $self->ip_addr($self->addr_block->next_free_addr->addr); + if (not $self->ip_addr) { + return "No free addresses in addr_block (blocknum: ".$self->blocknum.")"; + } + } + + # This should catch errors in the ip_addr. If it doesn't, + # they'll almost certainly not map into the block anyway. + my $self_addr = $self->NetAddr; #netmask is /32 + return ('Cannot parse address: ' . $self->ip_addr) unless $self_addr; + + my $block_addr = $self->addr_block->NetAddr; + unless ($block_addr->contains($self_addr)) { + return 'blocknum '.$self->blocknum.' does not contain address '.$self->ip_addr; + } + + my $router = $self->addr_block->router + or return 'Cannot assign address from unallocated block:'.$self->addr_block->blocknum; + if(grep { $_->routernum == $router->routernum} $self->allowed_routers) { + } # do nothing + else { + return 'Router '.$router->routernum.' cannot provide svcpart '.$self->svcpart; + } + + $self->SUPER::check; +} + +=item NetAddr + +Returns a NetAddr::IP object containing the IP address of this service. The netmask +is /32. + +=cut + +sub NetAddr { + my $self = shift; + return new NetAddr::IP ($self->ip_addr); +} + +=item addr_block + +Returns the FS::addr_block record (i.e. the address block) for this broadband service. + +=cut + +sub addr_block { + my $self = shift; + + return qsearchs('addr_block', { blocknum => $self->blocknum }); +} + +=back + +=item allowed_routers + +Returns a list of allowed FS::router objects. + +=cut + +sub allowed_routers { + my $self = shift; + + return map { $_->router } qsearch('part_svc_router', { svcpart => $self->svcpart }); +} + +=head1 BUGS + +The business with sb_field has been 'fixed', in a manner of speaking. + +=head1 SEE ALSO + +FS::svc_Common, FS::Record, FS::addr_block, +FS::part_svc, schema.html from the base documentation. + +=cut + +1; + diff --git a/FS/FS/svc_domain.pm b/FS/FS/svc_domain.pm index b06d03013..10d5d8f5c 100644 --- a/FS/FS/svc_domain.pm +++ b/FS/FS/svc_domain.pm @@ -1,16 +1,13 @@ package FS::svc_domain; use strict; -use vars qw( @ISA $whois_hack $conf $smtpmachine +use vars qw( @ISA $whois_hack $conf @defaultrecords $soadefaultttl $soaemail $soaexpire $soamachine - $soarefresh $soaretry $qshellmachine $nossh_hack + $soarefresh $soaretry ); use Carp; -use Mail::Internet 1.44; -use Mail::Header; use Date::Format; use Net::Whois 1.0; -use Net::SSH; use FS::Record qw(fields qsearch qsearchs dbh); use FS::Conf; use FS::svc_Common; @@ -27,8 +24,6 @@ use FS::queue; $FS::UID::callback{'FS::domain'} = sub { $conf = new FS::Conf; - $smtpmachine = $conf->config('smtpmachine'); - @defaultrecords = $conf->config('defaultrecords'); $soadefaultttl = $conf->config('soadefaultttl'); $soaemail = $conf->config('soaemail'); @@ -37,9 +32,6 @@ $FS::UID::callback{'FS::domain'} = sub { $soarefresh = $conf->config('soarefresh'); $soaretry = $conf->config('soaretry'); - $qshellmachine = $conf->exists('qmailmachines') - ? $conf->config('shellmachine') - : ''; }; =head1 NAME @@ -120,21 +112,6 @@ If any records are defined in the I<defaultrecords> configuration file, appropriate records are added to the domain_record table (see L<FS::domain_record>). -If a machine is defined in the I<shellmachine> configuration value, the -I<qmailmachines> configuration file exists, and the I<catchall> field points -to an an account with a home directory (see L<FS::svc_acct>), the command: - - [ -e $dir/.qmail-$qdomain-defualt ] || { - touch $dir/.qmail-$qdomain-default; - chown $uid:$gid $dir/.qmail-$qdomain-default; - } - -is executed on shellmachine via ssh (see L<dot-qmail/"EXTENSION ADDRESSES">). -This behaviour can be supressed by setting $FS::svc_domain::nossh_hack true. - -a machine is defined -in the - =cut sub insert { @@ -211,28 +188,6 @@ sub insert { $dbh->commit or die $dbh->errstr if $oldAutoCommit; - if ( $qshellmachine && $self->catchall && ! $nossh_hack ) { - - my $svc_acct = qsearchs( 'svc_acct', { 'svcnum' => $self->catchall } ) - or warn "WARNING: inserted unknown catchall: ". $self->catchall; - if ( $svc_acct && $svc_acct->dir ) { - my $qdomain = $self->domain; - $qdomain =~ s/\./:/g; #see manpage for 'dot-qmail': EXTENSION ADDRESSES - my ( $uid, $gid, $dir ) = ( - $svc_acct->uid, - $svc_acct->gid, - $svc_acct->dir, - ); - - my $queue = new FS::queue { - 'svcnum' => $self->svcnum, - 'job' => 'Net::SSH::ssh_cmd', - }; - $error = $queue->insert("root\@$qshellmachine", "[ -e $dir/.qmail-$qdomain-default ] || { touch $dir/.qmail-$qdomain-default; chown $uid:$gid $dir/.qmail-$qdomain-default; }" ); - - } - } - ''; #no error } @@ -251,10 +206,6 @@ sub delete { return "Can't delete a domain which has accounts!" if qsearch( 'svc_acct', { 'domsvc' => $self->svcnum } ); - return "Can't delete a domain with (svc_acct_sm) mail aliases!" - if defined( $FS::Record::dbdef->table('svc_acct_sm') ) - && qsearch('svc_acct_sm', { 'domsvc' => $self->svcnum } ); - #return "Can't delete a domain with (domain_record) zone entries!" # if qsearch('domain_record', { 'svcnum' => $self->svcnum } ); @@ -269,12 +220,6 @@ sub delete { local $FS::UID::AutoCommit = 0; my $dbh = dbh; - my $error = $self->SUPER::delete; - if ( $error ) { - $dbh->rollback if $oldAutoCommit; - return $error; - } - foreach my $domain_record ( reverse $self->domain_record ) { my $error = $domain_record->delete; if ( $error ) { @@ -282,6 +227,13 @@ sub delete { return $error; } } + + my $error = $self->SUPER::delete; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + $dbh->commit or die $dbh->errstr if $oldAutoCommit; } @@ -372,7 +324,7 @@ sub check { } #if ( $recref->{domain} =~ /^([\w\-\.]{1,22})\.(com|net|org|edu)$/ ) { - if ( $recref->{domain} =~ /^([\w\-]{1,22})\.(com|net|org|edu)$/ ) { + if ( $recref->{domain} =~ /^([\w\-]{1,63})\.(com|net|org|edu)$/ ) { $recref->{domain} = "$1.$2"; # hmmmmmmmm. } elsif ( $whois_hack && $recref->{domain} =~ /^([\w\-\.]+)$/ ) { @@ -385,10 +337,13 @@ sub check { $recref->{action} =~ /^(M|N)$/ or return "Illegal action"; $recref->{action} = $1; - my $svc_acct = qsearchs( 'svc_acct', { 'svcnum' => $recref->{catchall} } ); - return "Unknown catchall" unless $svc_acct || ! $recref->{catchall}; + if ( $recref->{catchall} ne '' ) { + my $svc_acct = qsearchs( 'svc_acct', { 'svcnum' => $recref->{catchall} } ); + return "Unknown catchall" unless $svc_acct; + } - $self->ut_textn('purpose'); + $self->ut_textn('purpose') + or $self->SUPER::check; } @@ -412,6 +367,15 @@ sub domain_record { } +sub catchall_svc_acct { + my $self = shift; + if ( $self->catchall ) { + qsearchs( 'svc_acct', { 'svcnum' => $self->catchall } ); + } else { + ''; + } +} + =item whois Returns the Net::Whois::Domain object (see L<Net::Whois>) for this domain, or @@ -448,14 +412,8 @@ sub submit_internic { =back -=head1 VERSION - -$Id: svc_domain.pm,v 1.31 2002-06-10 02:52:48 ivan Exp $ - =head1 BUGS -All BIND/DNS fields should be included (and exported). - Delete doesn't send a registration template. All registries should be supported. @@ -467,9 +425,8 @@ The $recref stuff in sub check should be cleaned up. =head1 SEE ALSO L<FS::svc_Common>, L<FS::Record>, L<FS::Conf>, L<FS::cust_svc>, -L<FS::part_svc>, L<FS::cust_pkg>, L<Net::Whois>, L<ssh>, -L<dot-qmail>, schema.html from the base documentation, config.html from the -base documentation. +L<FS::part_svc>, L<FS::cust_pkg>, L<Net::Whois>, schema.html from the base +documentation, config.html from the base documentation. =cut diff --git a/FS/FS/svc_external.pm b/FS/FS/svc_external.pm new file mode 100644 index 000000000..fe4ea1d67 --- /dev/null +++ b/FS/FS/svc_external.pm @@ -0,0 +1,174 @@ +package FS::svc_external; + +use strict; +use vars qw(@ISA); # $conf +use FS::UID; +#use FS::Record qw( qsearch qsearchs dbh); +use FS::svc_Common; + +@ISA = qw( FS::svc_Common ); + +#FS::UID::install_callback( sub { +# $conf = new FS::Conf; +#}; + +=head1 NAME + +FS::svc_external - Object methods for svc_external records + +=head1 SYNOPSIS + + use FS::svc_external; + + $record = new FS::svc_external \%hash; + $record = new FS::svc_external { 'column' => 'value' }; + + $error = $record->insert; + + $error = $new_record->replace($old_record); + + $error = $record->delete; + + $error = $record->check; + + $error = $record->suspend; + + $error = $record->unsuspend; + + $error = $record->cancel; + +=head1 DESCRIPTION + +An FS::svc_external object represents a externally tracked service. +FS::svc_external inherits from FS::svc_Common. The following fields are +currently supported: + +=over 4 + +=item svcnum - primary key + +=item id - unique number of external record + +=item title - for invoice line items + +=back + +=head1 METHODS + +=over 4 + +=item new HASHREF + +Creates a new external service. To add the external service to the database, +see L<"insert">. + +Note that this stores the hash reference, not a distinct copy of the hash it +points to. You can ask the object for a copy with the I<hash> method. + +=cut + +sub table { 'svc_external'; } + +=item insert + +Adds this external service to the database. If there is an error, returns the +error, otherwise returns false. + +The additional fields pkgnum and svcpart (see L<FS::cust_svc>) should be +defined. An FS::cust_svc record will be created and inserted. + +=cut + +sub insert { + my $self = shift; + my $error; + + $error = $self->SUPER::insert; + return $error if $error; + + ''; +} + +=item delete + +Delete this record from the database. + +=cut + +sub delete { + my $self = shift; + my $error; + + $error = $self->SUPER::delete; + return $error if $error; + + ''; +} + + +=item replace OLD_RECORD + +Replaces the OLD_RECORD with this one in the database. If there is an error, +returns the error, otherwise returns false. + +=cut + +sub replace { + my ( $new, $old ) = ( shift, shift ); + my $error; + + $error = $new->SUPER::replace($old); + return $error if $error; + + ''; +} + +=item suspend + +Called by the suspend method of FS::cust_pkg (see L<FS::cust_pkg>). + +=item unsuspend + +Called by the unsuspend method of FS::cust_pkg (see L<FS::cust_pkg>). + +=item cancel + +Called by the cancel method of FS::cust_pkg (see L<FS::cust_pkg>). + +=item check + +Checks all fields to make sure this is a valid external service. If there is +an error, returns the error, otherwise returns false. Called by the insert +and repalce methods. + +=cut + +sub check { + my $self = shift; + + my $x = $self->setfixed; + return $x unless ref($x); + my $part_svc = $x; + + my $error = + $self->ut_numbern('svcnum') + || $self->ut_number('id') + || $self->ut_textn('title') + ; + + $self->SUPER::check; +} + +=back + +=head1 BUGS + +=head1 SEE ALSO + +L<FS::svc_Common>, L<FS::Record>, L<FS::cust_svc>, L<FS::part_svc>, +L<FS::cust_pkg>, schema.html from the base documentation. + +=cut + +1; + diff --git a/FS/FS/svc_forward.pm b/FS/FS/svc_forward.pm index 1c5b5c40d..b9e8ff8f7 100644 --- a/FS/FS/svc_forward.pm +++ b/FS/FS/svc_forward.pm @@ -1,9 +1,7 @@ package FS::svc_forward; use strict; -use vars qw( @ISA $nossh_hack $conf $shellmachine @qmailmachines - @vpopmailmachines ); -use Net::SSH qw(ssh); +use vars qw( @ISA ); use FS::Conf; use FS::Record qw( fields qsearch qsearchs dbh ); use FS::svc_Common; @@ -13,21 +11,6 @@ use FS::svc_domain; @ISA = qw( FS::svc_Common ); -#ask FS::UID to run this stuff for us later -$FS::UID::callback{'FS::svc_forward'} = sub { - $conf = new FS::Conf; - if ( $conf->exists('qmailmachines') ) { - $shellmachine = $conf->config('shellmachine') - } else { - $shellmachine = ''; - } - if ( $conf->exists('vpopmailmachines') ) { - @vpopmailmachines = $conf->config('vpopmailmachines'); - } else { - @vpopmailmachines = (); - } -}; - =head1 NAME FS::svc_forward - Object methods for svc_forward records @@ -64,9 +47,11 @@ inherits from FS::Record. The following fields are currently supported: =item srcsvc - svcnum of the source of the forward (see L<FS::svc_acct>) +=item src - literal source (username or full email address) + =item dstsvc - svcnum of the destination of the forward (see L<FS::svc_acct>) -=item dst - foreign destination (email address) - forward not local to freeside +=item dst - literal destination (username or full email address) =back @@ -91,17 +76,6 @@ the error, otherwise returns false. The additional fields pkgnum and svcpart (see L<FS::cust_svc>) should be defined. An FS::cust_svc record will be created and inserted. -If the configuration value (see L<FS::Conf>) vpopmailmachines exists, then -the command: - - [ -d $vpopdir/domains/$domain/$source ] && { - echo "$destination" >> $vpopdir/domains/$domain/$username/.$qmail - chown $vpopuid:$vpopgid $vpopdir/domains/$domain/$username/.$qmail - } - -is executed on each vpopmailmachine via ssh (see the vpopmail documentation). -This behaviour can be supressed by setting $FS::svc_forward::nossh_hack true. - =cut sub insert { @@ -128,32 +102,6 @@ sub insert { return $error; } - my $svc_acct = qsearchs( 'svc_acct', { 'svcnum' => $self->srcsvc } ); - my $username = $svc_acct->username; - my $domain = $svc_acct->domain; - my $destination; - if ($self->dstsvc) { - $destination = $self->dstsvc_acct->email; - } else { - $destination = $self->dst; - } - - foreach my $vpopmailmachine ( @vpopmailmachines ) { - my($machine, $vpopdir, $vpopuid, $vpopgid) = split(/\s+/, $vpopmailmachine); - my $queue = new FS::queue { - 'svcnum' => $self->svcnum, - 'job' => 'Net::SSH::ssh_cmd', - }; - # should be neater - my $error = $queue->insert("root\@$machine","[ -d $vpopdir/domains/$domain/$username ] && { echo \"$destination\" >> $vpopdir/domains/$domain/$username/.qmail; chown $vpopuid:$vpopgid $vpopdir/domains/$domain/$username/.qmail; }") - unless $nossh_hack; - if ( $error ) { - $dbh->rollback if $oldAutoCommit; - return "queueing job (transaction rolled back): $error"; - } - - } - $dbh->commit or die $dbh->errstr if $oldAutoCommit; ''; #no error @@ -166,19 +114,6 @@ returns the error, otherwise returns false. The corresponding FS::cust_svc record will be deleted as well. -If the configuration value vpopmailmachines exists, then the command: - - { sed -e '/^$destination/d' < - $vpopdir/domains/$srcdomain/$srcusername/.qmail > - $vpopdir/domains/$srcdomain/$srcusername/.qmail.temp; - mv $vpopdir/domains/$srcdomain/$srcusername/.qmail.temp - $vpopdir/domains/$srcdomain/$srcusername/.qmail; - chown $vpopuid.$vpopgid $vpopdir/domains/$srcdomain/$srcusername/.qmail; } - - -is executed on each vpopmailmachine via ssh. This behaviour can be supressed -by setting $FS::svc_forward_nossh_hack true. - =cut sub delete { @@ -201,37 +136,6 @@ sub delete { return $error; } - my $svc_acct = $self->srcsvc_acct; - my $username = $svc_acct->username; - my $domain = $svc_acct->domain; - my $destination; - if ($self->dstsvc) { - $destination = $self->dstsvc_acct->email; - } else { - $destination = $self->dst; - } - foreach my $vpopmailmachine ( @vpopmailmachines ) { - my($machine, $vpopdir, $vpopuid, $vpopgid) = - split(/\s+/, $vpopmailmachine); - my $queue = new FS::queue { 'job' => 'Net::SSH::ssh_cmd' }; - # should be neater - my $error = $queue->insert("root\@$machine", - "sed -e '/^$destination/d' " . - "< $vpopdir/domains/$domain/$username/.qmail" . - "> $vpopdir/domains/$domain/$username/.qmail.temp; " . - "mv $vpopdir/domains/$domain/$username/.qmail.temp " . - "$vpopdir/domains/$domain/$username/.qmail; " . - "chown $vpopuid.$vpopgid $vpopdir/domains/$domain/$username/.qmail;" - ) - unless $nossh_hack; - - if ($error ) { - $dbh->rollback if $oldAutoCommit; - return "queueing job (transaction rolled back): $error"; - } - - } - $dbh->commit or die $dbh->errstr if $oldAutoCommit; ''; } @@ -242,29 +146,6 @@ sub delete { Replaces OLD_RECORD with this one in the database. If there is an error, returns the error, otherwise returns false. -If the configuration value vpopmailmachines exists, then the command: - - { sed -e '/^$destination/d' < - $vpopdir/domains/$srcdomain/$srcusername/.qmail > - $vpopdir/domains/$srcdomain/$srcusername/.qmail.temp; - mv $vpopdir/domains/$srcdomain/$srcusername/.qmail.temp - $vpopdir/domains/$srcdomain/$srcusername/.qmail; - chown $vpopuid.$vpopgid $vpopdir/domains/$srcdomain/$srcusername/.qmail; } - - -is executed on each vpopmailmachine via ssh. This behaviour can be supressed -by setting $FS::svc_forward_nossh_hack true. - -Also, if the configuration value vpopmailmachines exists, then the command: - - [ -d $vpopdir/domains/$domain/$source ] && { - echo "$destination" >> $vpopdir/domains/$domain/$username/.$qmail - chown $vpopuid:$vpopgid $vpopdir/domains/$domain/$username/.$qmail - } - -is executed on each vpopmailmachine via ssh. This behaviour can be supressed -by setting $FS::svc_forward_nossh_hack true. - =cut sub replace { @@ -295,66 +176,6 @@ sub replace { return $error; } - my $old_svc_acct = $old->srcsvc_acct; - my $old_username = $old_svc_acct->username; - my $old_domain = $old_svc_acct->domain; - my $destination; - if ($old->dstsvc) { - $destination = $old->dstsvc_acct->email; - } else { - $destination = $old->dst; - } - foreach my $vpopmailmachine ( @vpopmailmachines ) { - my($machine, $vpopdir, $vpopuid, $vpopgid) = - split(/\s+/, $vpopmailmachine); - my $queue = new FS::queue { - 'svcnum' => $new->svcnum, - 'job' => 'Net::SSH::ssh_cmd', - }; - # should be neater - my $error = $queue->insert("root\@$machine", - "sed -e '/^$destination/d' " . - "< $vpopdir/domains/$old_domain/$old_username/.qmail" . - "> $vpopdir/domains/$old_domain/$old_username/.qmail.temp; " . - "mv $vpopdir/domains/$old_domain/$old_username/.qmail.temp " . - "$vpopdir/domains/$old_domain/$old_username/.qmail; " . - "chown $vpopuid.$vpopgid " . - "$vpopdir/domains/$old_domain/$old_username/.qmail;" - ) - unless $nossh_hack; - - if ( $error ) { - $dbh->rollback if $oldAutoCommit; - return "queueing job (transaction rolled back): $error"; - } - } - - #false laziness with stuff in insert, should subroutine - my $svc_acct = qsearchs( 'svc_acct', { 'svcnum' => $new->srcsvc } ); - my $username = $svc_acct->username; - my $domain = $svc_acct->domain; - if ($new->dstsvc) { - $destination = $new->dstsvc_acct->email; - } else { - $destination = $new->dst; - } - - foreach my $vpopmailmachine ( @vpopmailmachines ) { - my($machine, $vpopdir, $vpopuid, $vpopgid) = split(/\s+/, $vpopmailmachine); - my $queue = new FS::queue { - 'svcnum' => $new->svcnum, - 'job' => 'Net::SSH::ssh_cmd', - }; - # should be neater - my $error = $queue->insert("root\@$machine","[ -d $vpopdir/domains/$domain/$username ] && { echo \"$destination\" >> $vpopdir/domains/$domain/$username/.qmail; chown $vpopuid:$vpopgid $vpopdir/domains/$domain/$username/.qmail; }") - unless $nossh_hack; - if ( $error ) { - $dbh->rollback if $oldAutoCommit; - return "queueing job (transaction rolled back): $error"; - } - } - #end subroutinable bits - $dbh->commit or die $dbh->errstr if $oldAutoCommit; ''; } @@ -395,12 +216,19 @@ sub check { #my $part_svc = $x; my $error = $self->ut_numbern('svcnum') - || $self->ut_number('srcsvc') + || $self->ut_numbern('srcsvc') || $self->ut_numbern('dstsvc') ; return $error if $error; - return "Unknown srcsvc" unless $self->srcsvc_acct; + return "Both srcsvc and src were defined; only one can be specified" + if $self->srcsvc && $self->src; + + return "one of srcsvc or src is required" + unless $self->srcsvc || $self->src; + + return "Unknown srcsvc: ". $self->srcsvc + unless ! $self->srcsvc || $self->srcsvc_acct; return "Both dstsvc and dst were defined; only one can be specified" if $self->dstsvc && $self->dst; @@ -408,26 +236,35 @@ sub check { return "one of dstsvc or dst is required" unless $self->dstsvc || $self->dst; - #return "Unknown dstsvc: $dstsvc" unless $self->dstsvc_acct || ! $self->dstsvc; - return "Unknown dstsvc" - unless qsearchs('svc_acct', { 'svcnum' => $self->dstsvc } ) - || ! $self->dstsvc; + return "Unknown dstsvc: ". $self->dstsvc + unless ! $self->dstsvc || $self->dstsvc_acct; + #return "Unknown dstsvc" + # unless qsearchs('svc_acct', { 'svcnum' => $self->dstsvc } ) + # || ! $self->dstsvc; + if ( $self->src ) { + $self->src =~ /^([\w\.\-\&]*)(\@([\w\-]+\.)+\w+)?$/ + or return "Illegal src: ". $self->dst; + $self->src("$1$2"); + } else { + $self->src(''); + } if ( $self->dst ) { - $self->dst =~ /^([\w\.\-]+)\@(([\w\-]+\.)+\w+)$/ + $self->dst =~ /^([\w\.\-\&]*)(\@([\w\-]+\.)+\w+)?$/ or return "Illegal dst: ". $self->dst; - $self->dst("$1\@$2"); + $self->dst("$1$2"); } else { $self->dst(''); } - ''; #no error + $self->SUPER::check; } =item srcsvc_acct -Returns the FS::svc_acct object referenced by the srcsvc column. +Returns the FS::svc_acct object referenced by the srcsvc column, or false for +literally specified forwards. =cut @@ -439,7 +276,7 @@ sub srcsvc_acct { =item dstsvc_acct Returns the FS::svc_acct object referenced by the srcsvc column, or false for -forwards not local to freeside. +literally specified forwards. =cut @@ -450,19 +287,12 @@ sub dstsvc_acct { =back -=head1 VERSION - -$Id: svc_forward.pm,v 1.12 2002-05-31 17:50:37 ivan Exp $ - =head1 BUGS -The remote commands should be configurable. - =head1 SEE ALSO L<FS::Record>, L<FS::Conf>, L<FS::cust_svc>, L<FS::part_svc>, L<FS::cust_pkg>, -L<FS::svc_acct>, L<FS::svc_domain>, L<Net::SSH>, L<ssh>, L<dot-qmail>, -schema.html from the base documentation. +L<FS::svc_acct>, L<FS::svc_domain>, schema.html from the base documentation. =cut diff --git a/FS/FS/svc_www.pm b/FS/FS/svc_www.pm index d7a42c8ae..7e8908346 100644 --- a/FS/FS/svc_www.pm +++ b/FS/FS/svc_www.pm @@ -234,7 +234,8 @@ sub check { return "Unknown usersvc (svc_acct.svcnum): ". $self->usersvc unless qsearchs('svc_acct', { 'svcnum' => $self->usersvc } ); - ''; #no error + $self->SUPER::check; + } =item domain_record diff --git a/FS/FS/type_pkgs.pm b/FS/FS/type_pkgs.pm index 8e0d4ef56..5b3b11c09 100644 --- a/FS/FS/type_pkgs.pm +++ b/FS/FS/type_pkgs.pm @@ -91,14 +91,27 @@ sub check { return "Unknown pkgpart" unless qsearchs( 'part_pkg', { 'pkgpart' => $self->pkgpart } ); - ''; #no error + $self->SUPER::check; } +=item part_pkg + +Returns the FS::part_pkg object associated with this record. + +=cut + +sub part_pkg { + my $self = shift; + qsearchs( 'part_pkg', { 'pkgpart' => $self->pkgpart } ); +} + +=cut + =back =head1 VERSION -$Id: type_pkgs.pm,v 1.1 1999-08-04 09:03:53 ivan Exp $ +$Id: type_pkgs.pm,v 1.3 2003-08-05 00:20:48 khoff Exp $ =head1 BUGS diff --git a/FS/MANIFEST b/FS/MANIFEST index 8355e40fb..3cbf0e91f 100644 --- a/FS/MANIFEST +++ b/FS/MANIFEST @@ -3,20 +3,30 @@ MANIFEST MANIFEST.SKIP Makefile.PL README +bin/freeside-addoutsource +bin/freeside-addoutsourceuser +bin/freeside-adduser +bin/freeside-apply-credits bin/freeside-bill +bin/freeside-cc-receipts-report +bin/freeside-count-active-customers +bin/freeside-credit-report bin/freeside-daily +bin/freeside-deloutsource +bin/freeside-deloutsourceuser +bin/freeside-deluser bin/freeside-email +bin/freeside-expiration-alerter bin/freeside-queued -bin/freeside-apply-credits -bin/freeside-adduser +bin/freeside-radgroup +bin/freeside-reexport +bin/freeside-selfservice-server bin/freeside-setinvoice -bin/freeside-overdue -bin/freeside-receivables-report +bin/freeside-setup +bin/freeside-sqlradius-radacctd +bin/freeside-sqlradius-reset +bin/freeside-sqlradius-seconds bin/freeside-tax-report -bin/freeside-cc-receipts-report -bin/freeside-credit-report -bin/freeside-expiration-alerter -bin/freeside-reexport FS.pm FS/CGI.pm FS/InitHandler.pm @@ -25,6 +35,7 @@ FS/ClientAPI/passwd.pm FS/ClientAPI/MyAccount.pm FS/Conf.pm FS/ConfItem.pm +FS/Misc.pm FS/Record.pm FS/SearchCache.pm FS/UI/Base.pm @@ -33,10 +44,12 @@ FS/UI/Gtk.pm FS/UI/agent.pm FS/UID.pm FS/Msgcat.pm +FS/acct_snarf.pm FS/agent.pm FS/agent_type.pm FS/cust_bill.pm FS/cust_bill_pkg.pm +FS/cust_bill_pkg_detail.pm FS/cust_credit.pm FS/cust_credit_bill.pm FS/cust_main.pm @@ -54,13 +67,19 @@ FS/part_bill_event.pm FS/export_svc.pm FS/part_export.pm FS/part_export_option.pm +FS/part_export/apache.pm FS/part_export/bind.pm FS/part_export/bind_slave.pm FS/part_export/bsdshell.pm +FS/part_export/communigate_pro.pm +FS/part_export/communigate_pro_singledomain.pm FS/part_export/cp.pm FS/part_export/cyrus.pm +FS/part_export/domain_shellcommands.pm +FS/part_export/forward_shellcommands.pm FS/part_export/http.pm FS/part_export/infostreet.pm +FS/part_export/ldap.pm FS/part_export/null.pm FS/part_export/shellcommands.pm FS/part_export/shellcommands_withdomain.pm @@ -75,12 +94,16 @@ FS/part_pop_local.pm FS/part_referral.pm FS/part_svc.pm FS/part_svc_column.pm +FS/part_svc_router.pm +FS/part_virtual_field.pm FS/pkg_svc.pm FS/svc_Common.pm FS/svc_acct.pm FS/svc_acct_pop.pm -FS/svc_acct_sm.pm +FS/svc_broadband.pm FS/svc_domain.pm +FS/svc_external.pm +FS/router.pm FS/type_pkgs.pm FS/nas.pm FS/port.pm @@ -103,13 +126,16 @@ t/InitHandler.t t/ClientAPI.t t/Conf.t t/ConfItem.t +t/Misc.t t/Record.t t/UID.t t/Msgcat.t +t/SearchCache.t t/cust_bill.t t/cust_bill_event.t t/cust_bill_pay.t t/cust_bill_pkg.t +t/cust_bill_pkg_detail.t t/cust_credit.t t/cust_credit_bill.t t/cust_credit_refund.t @@ -121,6 +147,7 @@ t/cust_pay_batch.t t/cust_pkg.t t/cust_refund.t t/cust_svc.t +t/cust_tax_exempt.t t/domain_record.t t/nas.t t/part_bill_event.t @@ -132,8 +159,11 @@ t/part_export-bind_slave.t t/part_export-bsdshell.t t/part_export-cp.t t/part_export-cyrus.t +t/part_export-domain_shellcommands.t +t/part_export-forward_shellcommands.t t/part_export-http.t t/part_export-infostreet.t +t/part_export-ldap.t t/part_export-null.t t/part_export-shellcommands.t t/part_export-shellcommands_withdomain.t @@ -155,14 +185,15 @@ t/radius_usergroup.t t/session.t t/svc_acct.t t/svc_acct_pop.t -t/svc_acct_sm.t +t/svc_broadband.t t/svc_Common.t t/svc_domain.t +t/svc_external.t t/svc_forward.t t/svc_www.t t/type_pkgs.t t/queue.t t/queue_arg.t +t/queue_depend.t t/msgcat.t t/raddb.t -t/cust_tax_exempt.t diff --git a/FS/Makefile.PL b/FS/Makefile.PL index ab4c2281b..1647f8eef 100644 --- a/FS/Makefile.PL +++ b/FS/Makefile.PL @@ -5,4 +5,6 @@ WriteMakefile( 'NAME' => 'FS', 'VERSION_FROM' => 'FS.pm', # finds $VERSION 'EXE_FILES' => [ glob 'bin/*' ], + 'INSTALLSCRIPT' => '/usr/local/bin', + 'INSTALLSITEBIN' => '/usr/local/bin', ); diff --git a/FS/bin/freeside-addoutsource b/FS/bin/freeside-addoutsource new file mode 100644 index 000000000..5cec17f46 --- /dev/null +++ b/FS/bin/freeside-addoutsource @@ -0,0 +1,24 @@ +#!/bin/sh + +domain=$1 + +createdb $domain && \ +\ +mkdir /usr/local/etc/freeside/conf.DBI:Pg:host=localhost\;dbname=$domain && \ +\ +chown freeside /usr/local/etc/freeside/conf.DBI:Pg:host=localhost\;dbname=$domain && \ +\ +cp /home/ivan/freeside/conf/[a-z]* /usr/local/etc/freeside/conf.DBI:Pg:host=localhost\;dbname=$domain && \ +\ +touch /usr/local/etc/freeside/conf.DBI:Pg:host=localhost\;dbname=$domain/secrets && \ +\ +chown freeside /usr/local/etc/freeside/conf.DBI:Pg:host=localhost\;dbname=$domain/secrets && \ +\ +chmod 600 /usr/local/etc/freeside/conf.DBI:Pg:host=localhost\;dbname=$domain/secrets && \ +\ +echo -e "DBI:Pg:host=localhost;dbname=$domain\nfreeside\n" >/usr/local/etc/freeside/conf.DBI:Pg:host=localhost\;dbname=$domain/secrets && \ +\ +mkdir /usr/local/etc/freeside/counters.DBI:Pg:host=localhost\;dbname=$domain && \ +mkdir /usr/local/etc/freeside/cache.DBI:Pg:host=localhost\;dbname=$domain && \ +mkdir /usr/local/etc/freeside/export.DBI:Pg:host=localhost\;dbname=$domain + diff --git a/FS/bin/freeside-addoutsourceuser b/FS/bin/freeside-addoutsourceuser new file mode 100644 index 000000000..abb515b6f --- /dev/null +++ b/FS/bin/freeside-addoutsourceuser @@ -0,0 +1,15 @@ +#!/bin/sh + +username=$1 +domain=$2 +password=$3 + +freeside-adduser -h /usr/local/etc/freeside/htpasswd \ + -s conf.DBI:Pg:host=localhost\;dbname=$domain/secrets \ + -b \ + $username $password 2>/dev/null + +[ -e /usr/local/etc/freeside/dbdef.DBI:Pg:host=localhost\;dbname=$domain ] \ + || ( freeside-setup -s $username 2>/dev/null; \ + /home/ivan/freeside/bin/populate-msgcat $username 2>/dev/null ) + diff --git a/FS/bin/freeside-adduser b/FS/bin/freeside-adduser index 9d424634b..c3ee05b9b 100644 --- a/FS/bin/freeside-adduser +++ b/FS/bin/freeside-adduser @@ -1,33 +1,37 @@ #!/usr/bin/perl -w # -# $Id: freeside-adduser,v 1.4 2002-02-06 14:58:05 ivan Exp $ +# $Id: freeside-adduser,v 1.8 2002-09-27 05:36:29 ivan Exp $ use strict; -use vars qw($opt_h $opt_c $opt_s); +use vars qw($opt_h $opt_b $opt_c $opt_s); +use Fcntl qw(:flock); use Getopt::Std; my $FREESIDE_CONF = "/usr/local/etc/freeside"; -getopts("ch:s:"); +getopts("bch:s:"); die &usage if $opt_c && ! $opt_h; my $user = shift or die &usage; if ( $opt_h ) { my @args = ( 'htpasswd' ); + push @args, '-b' if $opt_b; push @args, '-c' if $opt_c; push @args, $opt_h, $user; + push @args, shift if $opt_b; system(@args) == 0 or die "htpasswd failed: $?"; } my $secretfile = $opt_s || 'secrets'; open(MAPSECRETS,">>$FREESIDE_CONF/mapsecrets") - or die "can't open $FREESIDE_CONF/mapsecrets: $!"; + and flock(MAPSECRETS,LOCK_EX) + or die "can't open $FREESIDE_CONF/mapsecrets: $!"; print MAPSECRETS "$user $secretfile\n"; close MAPSECRETS or die "can't close $FREESIDE_CONF/mapsecrets: $!"; sub usage { - die "Usage:\n\n freeside-adduser [ -h htpasswd_file [ -c ] ] [ -s secretfile ] username" + die "Usage:\n\n freeside-adduser [ -h htpasswd_file [ -c ] [ -b ] ] [ -s secretfile ] username" } =head1 NAME @@ -45,13 +49,15 @@ sales/tech folks) to the web interface, not for adding customer accounts. -h: Also call htpasswd for this user with the given filename - -c: Passed to htpasswd + -c: Passed to htpasswd(1) -s: Specify an alternate secret file + -b: same as htpasswd(1), probably insecure, not recommended + =head1 SEE ALSO -L<htpasswd>, base Freeside documentation +L<htpasswd>(1), base Freeside documentation =cut diff --git a/FS/bin/freeside-cc-receipts-report b/FS/bin/freeside-cc-receipts-report index 06e3aba81..136851aec 100755 --- a/FS/bin/freeside-cc-receipts-report +++ b/FS/bin/freeside-cc-receipts-report @@ -206,7 +206,7 @@ if($email && $opt_m) # subroutines sub untaint_argv { foreach $_ ( $[ .. $#ARGV ) { #untaint @ARGV - $ARGV[$_] =~ /^([\w\-\/ :]*)$/ || die "Illegal argument \"$ARGV[$_]\""; + $ARGV[$_] =~ /^([\w\-\/ :\.]*)$/ || die "Illegal argument \"$ARGV[$_]\""; $ARGV[$_]=$1; } } @@ -245,7 +245,7 @@ user: From the mapsecrets file - see config.html from the base documentation =head1 VERSION -$Id: freeside-cc-receipts-report,v 1.4 2002-03-07 19:50:23 jeff Exp $ +$Id: freeside-cc-receipts-report,v 1.5 2002-09-09 22:57:34 ivan Exp $ =head1 BUGS diff --git a/FS/bin/freeside-count-active-customers b/FS/bin/freeside-count-active-customers new file mode 100755 index 000000000..759085a73 --- /dev/null +++ b/FS/bin/freeside-count-active-customers @@ -0,0 +1,17 @@ +#!/bin/sh + +domain=$1 + +echo "\t +select count(*) from cust_main where + 0 < ( SELECT COUNT(*) FROM cust_pkg + WHERE cust_pkg.custnum = cust_main.custnum + AND ( cust_pkg.cancel IS NULL + OR cust_pkg.cancel = 0 + ) + ) + OR 0 = ( SELECT COUNT(*) FROM cust_pkg + WHERE cust_pkg.custnum = cust_main.custnum + ); +" | psql -U freeside -q $domain | head -1 + diff --git a/FS/bin/freeside-credit-report b/FS/bin/freeside-credit-report index 7699daf4d..410dabe8f 100755 --- a/FS/bin/freeside-credit-report +++ b/FS/bin/freeside-credit-report @@ -160,7 +160,7 @@ if($email && $opt_m) # subroutines sub untaint_argv { foreach $_ ( $[ .. $#ARGV ) { #untaint @ARGV - $ARGV[$_] =~ /^([\w\-\/ :]*)$/ || die "Illegal argument \"$ARGV[$_]\""; + $ARGV[$_] =~ /^([\w\-\/ :\.]*)$/ || die "Illegal argument \"$ARGV[$_]\""; $ARGV[$_]=$1; } } @@ -199,7 +199,7 @@ user: From the mapsecrets file - see config.html from the base documentation =head1 VERSION -$Id: freeside-credit-report,v 1.4 2002-03-07 19:50:24 jeff Exp $ +$Id: freeside-credit-report,v 1.5 2002-09-09 22:57:34 ivan Exp $ =head1 BUGS diff --git a/FS/bin/freeside-daily b/FS/bin/freeside-daily index 142b0c73a..5fb966665 100755 --- a/FS/bin/freeside-daily +++ b/FS/bin/freeside-daily @@ -4,50 +4,81 @@ use strict; use Fcntl qw(:flock); use Date::Parse; use Getopt::Std; -use FS::UID qw(adminsuidsetup driver_name dbh); +use FS::UID qw(adminsuidsetup driver_name dbh datasrc); use FS::Record qw(qsearch qsearchs); +use FS::Conf; use FS::cust_main; &untaint_argv; #what it sounds like (eww) -use vars qw($opt_d $opt_v); -getopts("d:v"); +use vars qw($opt_d $opt_v $opt_p $opt_s $opt_y); +getopts("p:d:vsy:"); my $user = shift or die &usage; adminsuidsetup $user; $FS::cust_main::Debug = 1 if $opt_v; +my %search; +$search{'payby'} = $opt_p if $opt_p; + my @cust_main = @ARGV - ? map { qsearchs('cust_main', { custnum => $_ } ) } @ARGV - : qsearch('cust_main', {} ) + ? map { qsearchs('cust_main', { custnum => $_, %search } ) } @ARGV + : qsearch('cust_main', \%search ) ; #we're at now now (and later). my($time)= $opt_d ? str2time($opt_d) : $^T; +$time += $opt_y * 86400 if $opt_y; my($cust_main,%saw); foreach $cust_main ( @cust_main ) { - my $error; + # $^T not $time because -d is for pre-printing invoices + foreach my $cust_pkg ( + grep { $_->expire && $_->expire <= $^T } $cust_main->ncancelled_pkgs + ) { + my $error = $cust_pkg->cancel; + warn "Error cancelling expired pkg ". $cust_pkg->pkgnum. " for custnum ". + $cust_main->custnum. ": $error" + if $error; + } - $error = $cust_main->bill( 'time' => $time ); + my $error = $cust_main->bill( 'time' => $time, + 'resetup' => $opt_s, ); warn "Error billing, custnum ". $cust_main->custnum. ": $error" if $error; $cust_main->apply_payments; $cust_main->apply_credits; - $error=$cust_main->collect( 'invoice_time' => $time ); + $error = $cust_main->collect( 'invoice_time' => $time ); warn "Error collecting, custnum". $cust_main->custnum. ": $error" if $error; } if ( driver_name eq 'Pg' ) { + dbh->{AutoCommit} = 1; #so we can vacuum foreach my $statement ( 'vacuum', 'vacuum analyze' ) { my $sth = dbh->prepare($statement) or die dbh->errstr; $sth->execute or die $sth->errstr; } } +#local hack +my $conf = new FS::Conf; +my $dest = $conf->config('dump-scpdest'); +if ( $dest ) { + datasrc =~ /dbname=([\w\.]+)$/ or die "unparsable datasrc ". datasrc; + my $database = $1; + eval "use Net::SCP qw(scp);"; + if ( driver_name eq 'Pg' ) { + system("pg_dump $database >/var/tmp/$database.sql") + } else { + die "database dumps not yet supported for ". driver_name; + } + scp("/var/tmp/$database.sql", $dest); + unlink "/var/tmp/$database.sql" or die $!; +} + # subroutines sub untaint_argv { @@ -69,7 +100,7 @@ freeside-daily - Run daily billing and invoice collection events. =head1 SYNOPSIS - freeside-daily [ -d 'date' ] user [ custnum custnum ... ] + freeside-daily [ -d 'date' ] [ -y days ] [ -p 'payby' ] [ -s ] [ -v ] user [ custnum custnum ... ] =head1 DESCRIPTION @@ -84,6 +115,17 @@ the bill and collect methods of a cust_main object. See L<FS::cust_main>. -d: Pretend it's 'date'. Date is in any format Date::Parse is happy with, but be careful. + -y: In addition to -d, which specifies an absolute date, the -y switch + specifies an offset, in days. For example, "-y 15" would increment the + "pretend date" 15 days from whatever was specified by the -d switch + (or now, if no -d switch was given). + + -p: Only process customers with the specified payby (I<CARD>, I<DCRD>, I<CHEK>, I<DCHK>, I<BILL>, I<COMP>, I<LECB>) + + -s: re-charge setup fees + + -v: enable debugging + user: From the mapsecrets file - see config.html from the base documentation custnum: if one or more customer numbers are specified, only bills those diff --git a/FS/bin/freeside-deloutsource b/FS/bin/freeside-deloutsource new file mode 100644 index 000000000..561853539 --- /dev/null +++ b/FS/bin/freeside-deloutsource @@ -0,0 +1,11 @@ +#!/bin/sh + +domain=$1 + +dropdb $domain && \ +rm -rf /usr/local/etc/freeside/conf.DBI:Pg:host=localhost\;dbname=$domain && \ +rm -rf /usr/local/etc/freeside/counters.DBI:Pg:host=localhost\;dbname=$domain && \ +rm -rf /usr/local/etc/freeside/cache.DBI:Pg:host=localhost\;dbname=$domain && \ +rm -rf /usr/local/etc/freeside/export.DBI:Pg:host=localhost\;dbname=$domain && \ +rm /usr/local/etc/freeside/dbdef.DBI:Pg:host=localhost\;dbname=$domain + diff --git a/FS/bin/freeside-deloutsourceuser b/FS/bin/freeside-deloutsourceuser new file mode 100644 index 000000000..96871e50c --- /dev/null +++ b/FS/bin/freeside-deloutsourceuser @@ -0,0 +1,6 @@ +#!/bin/sh + +username=$1 + +freeside-deluser -h /usr/local/etc/freeside/htpasswd $username 2>/dev/null + diff --git a/FS/bin/freeside-deluser b/FS/bin/freeside-deluser new file mode 100644 index 000000000..57d6ce165 --- /dev/null +++ b/FS/bin/freeside-deluser @@ -0,0 +1,64 @@ +#!/usr/bin/perl -w + +use strict; +use vars qw($opt_h); +use Fcntl qw(:flock); +use Getopt::Std; + +my $FREESIDE_CONF = "/usr/local/etc/freeside"; + +getopts("h:"); +my $user = shift or die &usage; + +if ( $opt_h ) { + open(HTPASSWD,"<$opt_h") + and flock(HTPASSWD,LOCK_EX) + or die "can't open $opt_h: $!"; + open(HTPASSWD_TMP,">$opt_h.tmp") or die "can't open $opt_h.tmp: $!"; + while (<HTPASSWD>) { + print HTPASSWD_TMP $_ unless /^$user:/; + } + close HTPASSWD_TMP; + rename "$opt_h.tmp", "$opt_h" or die $!; + flock(HTPASSWD,LOCK_UN); + close HTPASSWD; +} + +open(MAPSECRETS,"<$FREESIDE_CONF/mapsecrets") + and flock(MAPSECRETS,LOCK_EX) + or die "can't open $FREESIDE_CONF/mapsecrets: $!"; +open(MAPSECRETS_TMP,">>$FREESIDE_CONF/mapsecrets.tmp") + or die "can't open $FREESIDE_CONF/mapsecrets.tmp: $!"; +while (<MAPSECRETS>) { + print MAPSECRETS_TMP $_ unless /^$user\s/; +} +close MAPSECRETS_TMP; +rename "$FREESIDE_CONF/mapsecrets.tmp", "$FREESIDE_CONF/mapsecrets" or die $!; +flock(MAPSECRETS,LOCK_UN); +close MAPSECRETS; + +sub usage { + die "Usage:\n\n freeside-deluser [ -h htpasswd_file ] username" +} + +=head1 NAME + +freeside-deluser - Command line interface to add (freeside) users. + +=head1 SYNOPSIS + + freeside-deluser [ -h htpasswd_file ] username + +=head1 DESCRIPTION + +Adds a user to the Freeside billing system. This is for adding users (internal +sales/tech folks) to the web interface, not for adding customer accounts. + + -h: Also delete from the given htpasswd filename + +=head1 SEE ALSO + +L<freeside-adduser>, L<htpasswd>(1), base Freeside documentation + +=cut + diff --git a/FS/bin/freeside-email b/FS/bin/freeside-email index c7ff41114..400dc2ac7 100755 --- a/FS/bin/freeside-email +++ b/FS/bin/freeside-email @@ -12,11 +12,9 @@ my $user = shift or die &usage; adminsuidsetup $user; my $conf = new FS::Conf; -my $domain = $conf->config('domain'); my @svc_acct = qsearch('svc_acct', {}); -my @usernames = map $_->username, @svc_acct; -my @emails = map "$_\@$domain", @usernames; +my @emails = map $_->email, @svc_acct; print join("\n", @emails), "\n"; @@ -51,7 +49,7 @@ user: From the mapsecrets file - see config.html from the base documentation =head1 VERSION -$Id: freeside-email,v 1.1 2001-05-15 07:52:34 ivan Exp $ +$Id: freeside-email,v 1.2 2002-09-18 22:50:44 ivan Exp $ =head1 BUGS diff --git a/FS/bin/freeside-expiration-alerter b/FS/bin/freeside-expiration-alerter index ee3c1fb92..691fd3aa5 100755 --- a/FS/bin/freeside-expiration-alerter +++ b/FS/bin/freeside-expiration-alerter @@ -80,22 +80,24 @@ $alerter->compile() or die "can't compile template: Text::Template::ERROR"; # Now I can start looping foreach my $customer (@customers) { + my $paydate = $customer->getfield('paydate'); + next if $paydate =~ /^\s*$/; #skip empty expiration dates + my $custnum = $customer->getfield('custnum'); my $first = $customer->getfield('first'); my $last = $customer->getfield('last'); my $company = $customer->getfield('company'); my $payby = $customer->getfield('payby'); my $payinfo = $customer->getfield('payinfo'); - my $paydate = $customer->getfield('paydate'); my $daytime = $customer->getfield('daytime'); my $night = $customer->getfield('night'); - + my ($payyear,$paymonth,$payday) = split (/-/,$paydate); my $expire_time = timelocal(0,0,0,$payday,--$paymonth,$payyear); #credit cards expire at the end of the month/year of their exp date - if ($payby eq 'CARD') { + if ($payby eq 'CARD' || $payby eq 'DCRD') { ($paymonth < 11) ? $paymonth++ : ($paymonth=0, $payyear++); $expire_time = timelocal(0,0,0,$payday,$paymonth,$payyear); $expire_time--; @@ -125,7 +127,7 @@ foreach my $customer (@customers) $FS::alerter::_template::first = $first; $FS::alerter::_template::last = $last; $FS::alerter::_template::company = $company; - if ($payby eq 'CARD') { + if ($payby eq 'CARD' || $payby eq 'DCRD') { $FS::alerter::_template::payby = "credit card (" . substr($payinfo, 0, 2) . "xxxxxxxxxx" . substr($payinfo, -4) . ")"; @@ -200,7 +202,7 @@ user: From the mapsecrets file - see config.html from the base documentation =head1 VERSION -$Id: freeside-expiration-alerter,v 1.3 2002-04-16 09:38:19 ivan Exp $ +$Id: freeside-expiration-alerter,v 1.5 2003-04-21 20:53:57 ivan Exp $ =head1 BUGS diff --git a/FS/bin/freeside-overdue b/FS/bin/freeside-overdue deleted file mode 100755 index 116245f9c..000000000 --- a/FS/bin/freeside-overdue +++ /dev/null @@ -1,196 +0,0 @@ -#!/usr/bin/perl -w - -use strict; -use vars qw( $days_to_pay $cust_main $cust_pkg - $cust_svc $svc_acct ); -use Getopt::Std; -use FS::cust_main; -use FS::cust_pkg; -use FS::cust_svc; -use FS::svc_acct; -use FS::Record qw(qsearch qsearchs); -use FS::UID qw(adminsuidsetup); - -&untaint_argv; -my %opt; -getopts('ed:qpl:scbyoi', \%opt); -my $user = shift or die &usage; - -adminsuidsetup $user; - -my $now = time; #eventually take a time option like freeside-bill -my ($sec,$min,$hour,$mday,$mon,$year) = - (localtime($now) )[0,1,2,3,4,5]; -$mon++; -$year += 1900; - -foreach $cust_main ( qsearch('cust_main',{} ) ) { - - my ( $eyear, $emon, $eday ) = ( 2037, 12, 31 ); - if ( $cust_main->paydate =~ /^(\d{4})\-(\d{1,2})\-(\d{1,2})$/ - && $cust_main->payby eq 'BILL') { - ( $eyear, $emon, $eday ) = ( $1, $2, $3 ); - } - - if ( ( $opt{d} - && $cust_main->balance_date(time - $opt{d} * 86400) > 0 - && qsearchs( 'cust_pkg', { 'custnum' => $cust_main->custnum, - 'susp' => "" } ) ) - || ( $opt{e} - && $cust_main->payby eq 'BILL' - && ( $eyear < $year - || ( $eyear == $year && $emon < $mon ) ) ) - ) { - - unless ( $opt{q} ) { - print $cust_main->custnum, "\t", - $cust_main->last, "\t", $cust_main->first, "\t", - $cust_main->balance_date(time-$opt{d} * 86400); - } - - if ( $opt{p} && ! grep { $_ eq 'POST' } $cust_main->invoicing_list ) { - print "\n\tAdding postal invoicing" unless $opt{q}; - my @invoicing_list = $cust_main->invoicing_list; - push @invoicing_list, 'POST'; - $cust_main->invoicing_list(\@invoicing_list); - } - - if ( $opt{l} ) { - print "\n\tCharging late fee of \$$opt{l}" unless $opt{q}; - my $error = $cust_main->charge($opt{l}, 'Late fee'); - # comment or plandata with info so we don't redo the same late fee every - # day - } - - foreach $cust_pkg ( qsearch( 'cust_pkg', - { 'custnum' => $cust_main->custnum } ) ) { - - if ($opt{s}) { - print "\n\tSuspending pkgnum " . $cust_pkg->pkgnum unless $opt{q}; - $cust_pkg->suspend; - } - - if ($opt{c}) { - print "\n\tCancelling pkgnum " . $cust_pkg->pkgnum unless $opt{q}; - $cust_pkg->cancel; - } - - } - - if ( $opt{b} ) { - print "\n\tBilling" unless $opt{q}; - my $error = $cust_main->bill('time'=>$now); - warn "Error billing, customer #" . $cust_main->custnum . - ":" . $error if $error; - } - - if ( $opt{y} ) { - print "\n\tApplying outstanding payments and credits" unless $opt{q}; - $cust_main->apply_payments; - $cust_main->apply_credits; - } - - if ( $opt{o} ) { - print "\n\tCollecting" unless $opt{q}; - my $error = $cust_main->collect( - 'invoice_time' => $now, - 'batch_card' => $opt{i} ? 'no' : 'yes', - 'force_print' => 'yes', - ); - warn "Error collecting from customer #" . $cust_main->custnum. ":$error" - if $error; - } - - print "\n" unless $opt{q}; - - } - -} - -sub untaint_argv { - foreach $_ ( $[ .. $#ARGV ) { - $ARGV[$_] =~ /^([\w\-\/\.]*)$/ || die "Illegal arguement \"$ARGV[$_]\""; - $ARGV[$_]=$1; - } -} - -sub usage { - die "Usage:\n\n freeside-overdue [ -e ] [ -d days ] [ -q ] [ -p ] [ -l amount ] [ -s ] [ -c ] [ -b ] [ -y ] [ -o [ -i ] ] user\n"; -} - - -=head1 NAME - -freeside-overdue - Perform actions on overdue and/or expired accounts. - -=head1 SYNOPSIS - - freeside-overdue [ -e ] [ -d days ] [ -q ] [ -p ] [ -l amount ] [ -s ] [ -c ] [ -b ] [ -y ] [ -o [ -i ] ] user - -=head1 DESCRIPTION - -This script is deprecated in 1.4.0. You should use freeside-daily and invoice -events instead. - -Performs actions on overdue and/or expired accounts. - -Selection options (at least one selection option is required): - - -d: Customers with a balance due on invoices older than the supplied number - of days. Requires an integer argument. - - -e: Customers with a billing expiration date in the past. - -Action options: - - -q: Be quiet (by default, selected accounts are printed). - - -p: Add postal invoicing to the relevant customers. - - -l: Add a charge of the given amount to the relevant customers. - - -s: Suspend accounts. - - -c: Cancel accounts. - - -b: Bill customers (create invoices) - - -y: Apply unapplied payments and credits - - -o: Collect from customers (charge cards, print invoices) - - -i: real-time billing (as opposed to batch billing). only relevant - for credit cards. - - user: From the mapsecrets file - see config.html from the base documentation - -=head1 CRONTAB - -Example crontab entries: - -# suspend expired accounts -20 4 * * * freeside-overdue -e -s user - -# quietly add postal invoicing to customers over 30 days past due -20 4 * * * freeside-overdue -d 30 -p -q user - -# suspend accounts and charge a $10.23 fee for customers over 60 days past due -20 4 * * * freeside-overdue -d 60 -s -l 10.23 user - -# cancel accounts over 90 days past due -20 4 * * * freeside-overdue -d 90 -c user - -=head1 ORIGINAL AUTHORS - -Original disable-overdue version by mw/kwh: Mark W.? and Kristian Hoffmann ? - -Ivan seems to be turning it into the "do-everything" CLI. - -=head1 BUGS - -Hell now that this is the do-everything CLI it should have --longoptions - -=cut - -1; - diff --git a/FS/bin/freeside-queued b/FS/bin/freeside-queued index 83074b9e4..6ea27c05f 100644 --- a/FS/bin/freeside-queued +++ b/FS/bin/freeside-queued @@ -1,10 +1,10 @@ #!/usr/bin/perl -w use strict; -use vars qw( $log_file $sigterm $sigint $kids $max_kids ); +use vars qw( $log_file $sigterm $sigint $kids $max_kids %kids ); use subs qw( _die _logmsg ); use Fcntl qw(:flock); -use POSIX qw(setsid); +use POSIX qw(:sys_wait_h setsid); use Date::Format; use IO::File; use FS::UID qw(adminsuidsetup forksuidsetup driver_name dbh); @@ -15,7 +15,7 @@ use FS::queue_depend; # no autoloading just yet use FS::cust_main; use FS::svc_acct; -use Net::SSH 0.06; +use Net::SSH 0.07; use FS::part_export; $max_kids = '10'; #guess it should be a config file... @@ -28,8 +28,8 @@ my $pid_file = "/var/run/freeside-queued.pid"; &daemonize1; -sub REAPER { my $pid = wait; $SIG{CHLD} = \&REAPER; $kids--; } -$SIG{CHLD} = \&REAPER; +#sub REAPER { my $pid = wait; $SIG{CHLD} = \&REAPER; $kids--; } +#$SIG{CHLD} = \&REAPER; $sigterm = 0; $sigint = 0; @@ -65,9 +65,11 @@ warn "freeside-queued starting\n"; my $warnkids=0; while (1) { + &reap_kids; #prevent runaway forking if ( $kids >= $max_kids ) { warn "WARNING: maximum $kids children reached\n" unless $warnkids++; + &reap_kids; sleep 1; #waiting for signals is cheap next; } @@ -131,6 +133,7 @@ while (1) { if ( $pid ) { $kids++; + $kids{$pid} = 1; } else { #kid time #get new db handle @@ -230,6 +233,16 @@ sub daemonize2 { open STDERR, '>&STDOUT' or die "Can't dup stdout: $!"; } +sub reap_kids { + foreach my $pid ( keys %kids ) { + my $kid = waitpid($pid, WNOHANG); + if ( $kid > 0 ) { + $kids--; + delete $kids{$kid}; + } + } +} + =head1 NAME freeside-queued - Job queue daemon diff --git a/FS/bin/freeside-radgroup b/FS/bin/freeside-radgroup new file mode 100644 index 000000000..ed85626d2 --- /dev/null +++ b/FS/bin/freeside-radgroup @@ -0,0 +1,76 @@ +#!/usr/bin/perl -w + +use strict; +use FS::UID qw(adminsuidsetup); +use FS::Record qw(qsearch); +use FS::cust_svc; +use FS::svc_acct; + +&untaint_argv; #what it sounds like (eww) + +my($user, $action, $groupname, $svcpart) = @ARGV; + +adminsuidsetup $user; + +my @svc_acct = map { $_->svc_x } qsearch('cust_svc', { svcpart => $svcpart } ); + +if ( lc($action) eq 'add' ) { + foreach my $svc_acct ( @svc_acct ) { + my @groups = $svc_acct->radius_groups; + next if grep { $_ eq $groupname } @groups; + push @groups, $groupname; + my %hash = $svc_acct->hash; + $hash{usergroup} = \@groups; + my $new = new FS::svc_acct \%hash; + my $error = $new->replace($svc_acct); + die $error if $error; + } +} else { + die &usage; +} + +# subroutines + +sub untaint_argv { + foreach $_ ( $[ .. $#ARGV ) { #untaint @ARGV + $ARGV[$_] =~ /^(.*)$/ || die "Illegal arguement \"$ARGV[$_]\""; + $ARGV[$_]=$1; + } +} + +sub usage { + die "Usage:\n\n freeside-radgroup user action groupname svcpart\n"; +} + +=head1 NAME + +freeside-radgroup - Command line utility to manipulate radius groups + +=head1 SYNOPSIS + + freeside-addgroup user action groupname svcpart + +=head1 DESCRIPTION + + B<user> is a freeside user as added with freeside-adduser. + + B<command> is the action to take. Available actions are: I<add> + + B<groupname> is the group to add (or remove, etc.) + + B<svcpart> specifies which accounts will be updated. + +=head1 EXAMPLES + +freeside-radgroup freesideuser add groupname 3 + +Adds I<groupname> to all accounts with service definition 3. + +=head1 BUGS + +=head1 SEE ALSO + +L<freeside-adduser>, L<FS::svc_acct>, L<FS::part_svc> + +=cut + diff --git a/FS/bin/freeside-receivables-report b/FS/bin/freeside-receivables-report deleted file mode 100755 index b5a49031e..000000000 --- a/FS/bin/freeside-receivables-report +++ /dev/null @@ -1,217 +0,0 @@ -#!/usr/bin/perl -Tw - -use strict; -use Date::Parse; -use Time::Local; -use Getopt::Std; -use Text::Template; -use Net::SMTP; -use Mail::Header; -use Mail::Internet; -use FS::Conf; -use FS::UID qw(adminsuidsetup); -use FS::Record qw(qsearch); -use FS::cust_main; - - -&untaint_argv; #what it sounds like (eww) -use vars qw($opt_v $opt_p $opt_m $opt_e $opt_t $report_lines $report_template @buf $header); -getopts("vpmet:"); #switches - -#we're at now now (and later). -my($_date)= $^T; - -# Get the current month -my ($sec,$min,$hour,$mday,$mon,$year) = - (localtime($_date) )[0,1,2,3,4,5]; -$mon++; -$year += 1900; - -# Login to the database -my $user = shift or die &usage; -adminsuidsetup $user; - -# Get the needed configuration files -my $conf = new FS::Conf; -my $lpr = $conf->config('lpr'); -my $email = $conf->config('email'); -my $smtpmachine = $conf->config('smtpmachine'); -my $mail_sender = $conf->exists('invoice_from') ? $conf->config('invoice_from') : - 'postmaster'; -my @report_template = $conf->config('report_template') - or die "cannot load config file report_template"; -$report_lines = 0; - foreach ( grep /report_lines\(\d+\)/, @report_template ) { #kludgy :/ - /report_lines\((\d+)\)/; - $report_lines += $1; -} -die "no report_lines() functions in template?" unless $report_lines; -$report_template = new Text::Template ( - TYPE => 'ARRAY', - SOURCE => [ map "$_\n", @report_template ], -) or die "can't create new Text::Template object: $Text::Template::ERROR"; - - -my(@customers)=qsearch('cust_main',{}); -if (scalar(@customers) == 0) -{ - exit 1; -} - -# Open print and email pipes -# $lpr and opt_p for printing -# $email and opt_m for email - -if ($lpr && $opt_p) -{ - open(LPR, "|$lpr"); -} - -if ($email && $opt_m) -{ - $ENV{MAILADDRESS} = $mail_sender; - $header = new Mail::Header ( [ - "From: Account Processor", - "To: $email", - "Sender: $mail_sender", - "Reply-To: $mail_sender", - "Subject: Receivables", - ] ); -} - -my $total = 0; - - -# Now I can start looping -foreach my $customer (@customers) -{ - my $custnum = $customer->getfield('custnum'); - my $first = $customer->getfield('first'); - my $last = $customer->getfield('last'); - my $company = $customer->getfield('company'); - my $daytime = $customer->getfield('daytime'); - my $balance = $customer->balance; - - - if ($balance != 0) { - $total += $balance; - push @buf, sprintf(qq{%8d %-32.32s %12s %9.2f}, - $custnum, - $first . " " . $last . " " . $company, - $daytime, - $balance); - - } - -} - -push @buf, ('', sprintf(qq{%61s}, "========="), sprintf(qq{%61.2f}, $total)); - -sub FS::receivables_report::_template::report_lines { - my $lines = shift; - map { - scalar(@buf) ? shift @buf : '' ; - } - ( 1 .. $lines ); -} - -$FS::receivables_report::_template::title = " R E C E I V A B L E S "; -$FS::receivables_report::_template::title = $opt_t if $opt_t; -$FS::receivables_report::_template::page = 1; -$FS::receivables_report::_template::date = $_date; -$FS::receivables_report::_template::date = $_date; -$FS::receivables_report::_template::total_pages = - int( scalar(@buf) / $report_lines); -$FS::receivables_report::_template::total_pages++ if scalar(@buf) % $report_lines; - -my @report; -while (@buf) { - push @report, split("\n", - $report_template->fill_in( PACKAGE => 'FS::receivables_report::_template' ) - ); - $FS::receivables_report::_template::page++; -} - -if ($opt_v) { - print map "$_\n", @report; -} -if($lpr && $opt_p) -{ - print LPR map "$_\n", @report; - print LPR "\f" if $opt_e; - close LPR || die "Could not close printer: $lpr\n"; -} -if($email && $opt_m) -{ - my $message = new Mail::Internet ( - 'Header' => $header, - 'Body' => [ (@report) ], - ); - $!=0; - $message->smtpsend( Host => "$smtpmachine" ) - or die "can't send report to $email via $smtpmachine: $!"; -} - - -# subroutines - -sub untaint_argv { - foreach $_ ( $[ .. $#ARGV ) { #untaint @ARGV - $ARGV[$_] =~ /^([\w\-\/ ]*)$/ || die "Illegal argument \"$ARGV[$_]\""; - $ARGV[$_]=$1; - } -} - -sub usage { - die "Usage:\n\n freeside-receivables-report [-v] [-p] [-e] user\n"; -} - -=head1 NAME - -freeside-receivables-report - Prints or emails outstanding receivables. - -=head1 SYNOPSIS - - freeside-receivables-report [-v] [-p] [-m] [-e] [-t "title"] user - -=head1 DESCRIPTION - -Prints or emails outstanding receivables - -B<-v>: Verbose - Prints records to STDOUT. - -B<-p>: Print to printer lpr as found in the conf directory. - -B<-m>: Mail output to user found in the Conf email file. - -B<-e>: Print a final form feed to the printer. - -B<-t>: supply a title for the top of each page. - -user: From the mapsecrets file - see config.html from the base documentation - -=head1 VERSION - -$Id: freeside-receivables-report,v 1.5 2002-03-07 19:50:24 jeff Exp $ - -=head1 BUGS - -Yes..... Use at your own risk. No guarantees or warrantees of any -kind apply to this program. Parts of this program are hacked from -other GNU licensed software created mainly by Ivan Kohler. - -This is released under the GNU Public License. See www.gnu.org -for more information regarding this license. - -=head1 SEE ALSO - -L<FS::cust_main>, config.html from the base documentation - -=head1 AUTHOR - -Jeff Finucane <jeff@cmh.net> - -based on print-batch by Joel Griffiths <griff@aver-computer.com> - -=cut - diff --git a/FS/bin/freeside-selfservice-server b/FS/bin/freeside-selfservice-server new file mode 100644 index 000000000..371a646b4 --- /dev/null +++ b/FS/bin/freeside-selfservice-server @@ -0,0 +1,266 @@ +#!/usr/bin/perl -w +# +# freeside-selfservice-server + +# alas, much false laziness with freeside-queued and fs_signup_server. at +# least it is slated to replace fs_{signup,passwd,mailadmin}_server +# should probably generalize the version in here, or better yet use +# Proc::Daemon or somesuch + +use strict; +use vars qw( $Debug %kids $kids $max_kids $shutdown $log_file $ssh_pid ); +use subs qw( lock_write unlock_write ); +use Fcntl qw(:flock); +use POSIX qw(:sys_wait_h setsid); +use IO::Handle; +use IO::Select; +use IO::File; +use Storable qw(nstore_fd fd_retrieve); +use Net::SSH qw(sshopen2); +use FS::UID qw(adminsuidsetup forksuidsetup); +use FS::ClientAPI; + +use FS::Conf; +use FS::cust_bill; +use FS::cust_pkg; + +$Debug = 2; # >= 2 will log packet contents, including potentially compromising + # information + +$shutdown = 0; +$max_kids = '10'; #? +$kids = 0; + +my $user = shift or die &usage; +my $machine = shift or die &usage; +my $tag = scalar(@ARGV) ? shift : ''; + +# $FS::UID::datasrc not posible +my $pid_file = "/var/run/freeside-selfservice-server.$user.$machine.pid"; + +my $lock_file = "/usr/local/etc/freeside/selfservice.$machine.writelock"; +open(LOCKFILE,">$lock_file") or die "can't open $lock_file: $!"; + +&init($user); + +my $conf = new FS::Conf; + +my $clientd = "/usr/local/sbin/freeside-selfservice-clientd"; #better name? + +my $warnkids=0; +while (1) { + my($writer,$reader,$error) = (new IO::Handle, new IO::Handle, new IO::Handle); + warn "connecting to $machine\n" if $Debug; + + $ssh_pid = sshopen2($machine,$reader,$writer,$clientd,$tag); + +# nstore_fd(\*writer, {'hi'=>'there'}); + + warn "entering main loop\n" if $Debug; + my $undisp = 0; + my $s = IO::Select->new( $reader ); + while (1) { + + &reap_kids; + + warn "waiting for packet from client\n" if $Debug && !$undisp; + $undisp = 1; + my @handles = $s->can_read(5); + unless ( @handles ) { + &shutdown if $shutdown; + next; + } + + $undisp = 0; + + warn "receiving packet from client\n" if $Debug; + + my $packet = eval { fd_retrieve($reader); }; + if ( $@ ) { + warn "Storable error receiving packet from client". + " (assuming lost connection): $@\n" + if $Debug; + if ( $ssh_pid ) { + warn "sending TERM signal to ssh process $ssh_pid\n" if $Debug; + kill 'TERM', $ssh_pid; + $ssh_pid = 0; + } + last; + } + warn "packet received\n". + join('', map { " $_=>$packet->{$_}\n" } keys %$packet ) + if $Debug > 1; + + #prevent runaway forking + my $warnkids = 0; + while ( $kids >= $max_kids ) { + warn "WARNING: maximum $kids children reached\n" unless $warnkids++; + &reap_kids; + sleep 1; + } + + warn "forking child\n" if $Debug; + defined( my $pid = fork ) or die "can't fork: $!"; + if ( $pid ) { + $kids++; + $kids{$pid} = 1; + warn "child $pid spawned\n" if $Debug; + } else { #kid time + + #get new db handle + $FS::UID::dbh->{InactiveDestroy} = 1; + forksuidsetup($user); + + my $type = $packet->{_packet}; + warn "calling $type handler\n" if $Debug; + my $rv = eval { FS::ClientAPI->dispatch($type, $packet); }; + if ( $@ ) { + warn my $error = "WARNING: error dispatching $type: $@"; + $rv = { _error => $error }; + } + $rv->{_token} = $packet->{_token}; #identifier + + warn "sending response\n" if $Debug; + lock_write; + nstore_fd($rv, $writer) or die "FATAL: can't send response: $!"; + $writer->flush or die "FATAL: can't flush: $!"; + unlock_write; + + warn "child exiting\n" if $Debug; + exit; #end-of-kid + } + + } + + warn "connection lost, reconnecting\n" if $Debug; + sleep 3; + +} + +### +# utility subroutines +### + +sub reap_kids { + #warn "reaping kids\n"; + foreach my $pid ( keys %kids ) { + my $kid = waitpid($pid, WNOHANG); + if ( $kid > 0 ) { + $kids--; + delete $kids{$kid}; + } + } + #warn "done reaping\n"; +} + +sub init { + my $user = shift; + + chdir "/" or die "Can't chdir to /: $!"; + open STDIN, '/dev/null' or die "Can't read /dev/null: $!"; + defined(my $pid = fork) or die "Can't fork: $!"; + if ( $pid ) { + print "freeside-selfservice-server to $machine started with pid $pid\n"; #logging to $log_file + exit unless $pid_file; + my $pidfh = new IO::File ">$pid_file" or exit; + print $pidfh "$pid\n"; + exit; + } + +# sub REAPER { my $pid = wait; $SIG{CHLD} = \&REAPER; $kids--; } +# #sub REAPER { my $pid = wait; $kids--; $SIG{CHLD} = \&REAPER; } +# $SIG{CHLD} = \&REAPER; + + $shutdown = 0; + $SIG{HUP} = sub { warn "SIGHUP received; shutting down\n"; $shutdown++; }; + $SIG{INT} = sub { warn "SIGINT received; shutting down\n"; $shutdown++; }; + $SIG{TERM} = sub { warn "SIGTERM received; shutting down\n"; $shutdown++; }; + $SIG{QUIT} = sub { warn "SIGQUIT received; shutting down\n"; $shutdown++; }; + $SIG{PIPE} = sub { warn "SIGPIPE received; shutting down\n"; $shutdown++; }; + + #false laziness w/freeside-queued + my $freeside_gid = scalar(getgrnam('freeside')) + or die "can't setgid to freeside group\n"; + $) = $freeside_gid; + $( = $freeside_gid; + #if freebsd can't setuid(), presumably it can't setgid() either. grr fleabsd + ($(,$)) = ($),$(); + $) = $freeside_gid; + + $> = $FS::UID::freeside_uid; + $< = $FS::UID::freeside_uid; + #freebsd is sofa king broken, won't setuid() + ($<,$>) = ($>,$<); + $> = $FS::UID::freeside_uid; + #eslaf + + $ENV{HOME} = (getpwuid($>))[7]; #for ssh + adminsuidsetup $user; + + #$log_file = "/usr/local/etc/freeside/selfservice.". $FS::UID::datasrc; #MACHINE NAME + $log_file = "/usr/local/etc/freeside/selfservice.$machine.log"; + + open STDOUT, '>/dev/null' + or die "Can't write to /dev/null: $!"; + setsid or die "Can't start a new session: $!"; + open STDERR, '>&STDOUT' or die "Can't dup stdout: $!"; + + $SIG{__DIE__} = \&_die; + $SIG{__WARN__} = \&_logmsg; + + warn "freeside-selfservice-server starting\n"; + +} + +sub shutdown { + my $wait = 12; #wait up to 1 minute + while ( $kids > 0 && $wait-- ) { + warn "waiting for $kids children to terminate"; + sleep 5; + } + warn "abandoning $kids children" if $kids; + kill 'TERM', $ssh_pid if $ssh_pid; + die "exiting"; +} + +sub _die { + my $msg = shift; + unlink $pid_file if -e $pid_file; + _logmsg($msg); +} + +sub _logmsg { + chomp( my $msg = shift ); + _do_logmsg( "[server] [". scalar(localtime). "] [$$] $msg\n" ); +} + +sub _do_logmsg { + chomp( my $msg = shift ); + my $log = new IO::File ">>$log_file"; + flock($log, LOCK_EX); + seek($log, 0, 2); + print $log "$msg\n"; + flock($log, LOCK_UN); + close $log; +} + +sub lock_write { + #broken on freebsd? + #flock($writer, LOCK_EX) or die "FATAL: can't lock write stream: $!"; + + flock(LOCKFILE, LOCK_EX) or die "FATAL: can't lock $lock_file: $!"; + +} + +sub unlock_write { + #broken on freebsd? + #flock($writer, LOCK_UN) or die "WARNING: can't release write lock: $!"; + + flock(LOCKFILE, LOCK_UN) or die "FATAL: can't unlock $lock_file: $!"; + +} + +sub usage { + die "Usage:\n\n freeside-selfservice-server user machine\n"; +} + diff --git a/FS/bin/freeside-setup b/FS/bin/freeside-setup new file mode 100755 index 000000000..7512cc18c --- /dev/null +++ b/FS/bin/freeside-setup @@ -0,0 +1,1141 @@ +#!/usr/bin/perl -Tw + +#to delay loading dbdef until we're ready +BEGIN { $FS::Record::setup_hack = 1; } + +use strict; +use vars qw($opt_s); +use Getopt::Std; +use DBI; +use DBIx::DBSchema 0.21; +use DBIx::DBSchema::Table; +use DBIx::DBSchema::Column; +use DBIx::DBSchema::ColGroup::Unique; +use DBIx::DBSchema::ColGroup::Index; +use FS::UID qw(adminsuidsetup datasrc checkeuid getsecrets); +use FS::Record; +use FS::cust_main_county; +use FS::raddb; +use FS::part_bill_event; + +die "Not running uid freeside!" unless checkeuid(); + +my %attrib2db = + map { lc($FS::raddb::attrib{$_}) => $_ } keys %FS::raddb::attrib; + +getopts("s"); +my $user = shift or die &usage; +getsecrets($user); + +#needs to match FS::Record +my($dbdef_file) = "/usr/local/etc/freeside/dbdef.". datasrc; + +### + +#print "\nEnter the maximum username length: "; +#my($username_len)=&getvalue; +my $username_len = 32; #usernamemax config file + +#print "\n\n", <<END, ":"; +#Freeside tracks the RADIUS User-Name, check attribute Password and +#reply attribute Framed-IP-Address for each user. You can specify additional +#check and reply attributes (or you can add them later with the +#fs-radius-add-check and fs-radius-add-reply programs). +# +#First enter any additional RADIUS check attributes you need to track for each +#user, separated by whitespace. +#END +#my @check_attributes = map { $attrib2db{lc($_)} or die "unknown attribute $_"; } +# split(" ",&getvalue); +# +#print "\n\n", <<END, ":"; +#Now enter any additional reply attributes you need to track for each user, +#separated by whitespace. +#END +#my @attributes = map { $attrib2db{lc($_)} or die "unknown attribute $_"; } +# split(" ",&getvalue); +# +#print "\n\n", <<END, ":"; +#Do you wish to enable the tracking of a second, separate shipping/service +#address? +#END +#my $ship = &_yesno; +# +#sub getvalue { +# my($x)=scalar(<STDIN>); +# chop $x; +# $x; +#} +# +#sub _yesno { +# print " [y/N]:"; +# my $x = scalar(<STDIN>); +# $x =~ /^y/i; +#} + +my @check_attributes = (); #add later +my @attributes = (); #add later +my $ship = $opt_s; + +### + +my($char_d) = 80; #default maxlength for text fields + +#my(@date_type) = ( 'timestamp', '', '' ); +my(@date_type) = ( 'int', 'NULL', '' ); +my(@perl_type) = ( 'text', 'NULL', '' ); +my @money_type = ( 'decimal', '', '10,2' ); + +### +# create a dbdef object from the old data structure +### + +my(%tables)=&tables_hash_hack; + +#turn it into objects +my($dbdef) = new DBIx::DBSchema ( map { + my(@columns); + while (@{$tables{$_}{'columns'}}) { + my($name,$type,$null,$length)=splice @{$tables{$_}{'columns'}}, 0, 4; + push @columns, new DBIx::DBSchema::Column ( $name,$type,$null,$length ); + } + DBIx::DBSchema::Table->new( + $_, + $tables{$_}{'primary_key'}, + DBIx::DBSchema::ColGroup::Unique->new($tables{$_}{'unique'}), + DBIx::DBSchema::ColGroup::Index->new($tables{$_}{'index'}), + @columns, + ); +} (keys %tables) ); + +my $cust_main = $dbdef->table('cust_main'); +unless ($ship) { #remove ship_ from cust_main + $cust_main->delcolumn($_) foreach ( grep /^ship_/, $cust_main->columns ); +} else { #add indices + push @{$cust_main->index->lol_ref}, + map { [ "ship_$_" ] } qw( last company daytime night fax ); +} + +#add radius attributes to svc_acct + +my($svc_acct)=$dbdef->table('svc_acct'); + +my($attribute); +foreach $attribute (@attributes) { + $svc_acct->addcolumn ( new DBIx::DBSchema::Column ( + 'radius_'. $attribute, + 'varchar', + 'NULL', + $char_d, + )); +} + +foreach $attribute (@check_attributes) { + $svc_acct->addcolumn( new DBIx::DBSchema::Column ( + 'rc_'. $attribute, + 'varchar', + 'NULL', + $char_d, + )); +} + +#create history tables (false laziness w/create-history-tables) +foreach my $table ( grep { ! /^h_/ } $dbdef->tables ) { + my $tableobj = $dbdef->table($table) + or die "unknown table $table"; + + die "unique->lol_ref undefined for $table" + unless defined $tableobj->unique->lol_ref; + die "index->lol_ref undefined for $table" + unless defined $tableobj->index->lol_ref; + + my $h_tableobj = DBIx::DBSchema::Table->new( { + name => "h_$table", + primary_key => 'historynum', + unique => DBIx::DBSchema::ColGroup::Unique->new( [] ), + 'index' => DBIx::DBSchema::ColGroup::Index->new( [ + @{$tableobj->unique->lol_ref}, + @{$tableobj->index->lol_ref} + ] ), + columns => [ + DBIx::DBSchema::Column->new( { + 'name' => 'historynum', + 'type' => 'serial', + 'null' => 'NOT NULL', + 'length' => '', + 'default' => '', + 'local' => '', + } ), + DBIx::DBSchema::Column->new( { + 'name' => 'history_date', + 'type' => 'int', + 'null' => 'NULL', + 'length' => '', + 'default' => '', + 'local' => '', + } ), + DBIx::DBSchema::Column->new( { + 'name' => 'history_user', + 'type' => 'varchar', + 'null' => 'NOT NULL', + 'length' => '80', + 'default' => '', + 'local' => '', + } ), + DBIx::DBSchema::Column->new( { + 'name' => 'history_action', + 'type' => 'varchar', + 'null' => 'NOT NULL', + 'length' => '80', + 'default' => '', + 'local' => '', + } ), + map { + my $column = $tableobj->column($_); + + #clone so as to not disturb the original + $column = DBIx::DBSchema::Column->new( { + map { $_ => $column->$_() } + qw( name type null length default local ) + } ); + + $column->type('int') + if $column->type eq 'serial'; + #$column->default('') + # if $column->default =~ /^nextval\(/i; + #( my $local = $column->local ) =~ s/AUTO_INCREMENT//i; + #$column->local($local); + $column; + } $tableobj->columns + ], + } ); + $dbdef->addtable($h_tableobj); +} + +#important +$dbdef->save($dbdef_file); +&FS::Record::reload_dbdef($dbdef_file); + +### +# create 'em +### + +my($dbh)=adminsuidsetup $user; + +#create tables +$|=1; + +foreach my $statement ( $dbdef->sql($dbh) ) { + $dbh->do( $statement ) + or die "CREATE error: ". $dbh->errstr. "\ndoing statement: $statement"; +} + +#not really sample data (and shouldn't default to US) + +#cust_main_county + +#USPS state codes +foreach ( qw( +AL AK AS AZ AR CA CO CT DC DE FM FL GA GU HI ID IL IN IA KS KY LA +ME MH MD MA MI MN MS MO MT NC ND NE NH NJ NM NV NY MP OH OK OR PA PW PR RI +SC SD TN TX UT VT VI VA WA WV WI WY AE AA AP +) ) { + my($cust_main_county)=new FS::cust_main_county({ + 'state' => $_, + 'tax' => 0, + 'country' => 'US', + }); + my($error); + $error=$cust_main_county->insert; + die $error if $error; +} + +#AU "offical" state codes ala mark.williamson@ebbs.com.au (Mark Williamson) +foreach ( qw( +VIC NSW NT QLD TAS ACT WA SA +) ) { + my($cust_main_county)=new FS::cust_main_county({ + 'state' => $_, + 'tax' => 0, + 'country' => 'AU', + }); + my($error); + $error=$cust_main_county->insert; + die $error if $error; +} + +#ISO 2-letter country codes (same as country TLDs) except US and AU +foreach ( qw( +AF AL DZ AS AD AO AI AQ AG AR AM AW AT AZ BS BH BD BB BY BE BZ BJ BM BT BO +BA BW BV BR IO BN BG BF BI KH CM CA CV KY CF TD CL CN CX CC CO KM CG CK CR CI +HR CU CY CZ DK DJ DM DO TP EC EG SV GQ ER EE ET FK FO FJ FI FR FX GF PF TF GA +GM GE DE GH GI GR GL GD GP GU GT GN GW GY HT HM HN HK HU IS IN ID IR IQ IE IL +IT JM JP JO KZ KE KI KP KR KW KG LA LV LB LS LR LY LI LT LU MO MK MG MW MY MV +ML MT MH MQ MR MU YT MX FM MD MC MN MS MA MZ MM NA NR NP NL AN NC NZ NI NE NG +NU NF MP NO OM PK PW PA PG PY PE PH PN PL PT PR QA RE RO RU RW KN LC VC WS SM +ST SA SN SC SL SG SK SI SB SO ZA GS ES LK SH PM SD SR SJ SZ SE CH SY TW TJ TZ +TH TG TK TO TT TN TR TM TC TV UG UA AE GB UM UY UZ VU VA VE VN VG VI WF EH +YE YU ZR ZM ZW +) ) { + my($cust_main_county)=new FS::cust_main_county({ + 'tax' => 0, + 'country' => $_, + }); + my($error); + $error=$cust_main_county->insert; + die $error if $error; +} + +#billing events +foreach my $aref ( + [ 'COMP', 'Comp invoice', '$cust_bill->comp();', 30, 'comp' ], + [ 'CARD', 'Batch card', '$cust_bill->batch_card();', 40, 'batch-card' ], + [ 'BILL', 'Send invoice', '$cust_bill->send();', 50, 'send' ], + [ 'DCRD', 'Send invoice', '$cust_bill->send();', 50, 'send' ], + [ 'DCHK', 'Send invoice', '$cust_bill->send();', 50, 'send' ], +) { + + my $part_bill_event = new FS::part_bill_event({ + 'payby' => $aref->[0], + 'event' => $aref->[1], + 'eventcode' => $aref->[2], + 'seconds' => 0, + 'weight' => $aref->[3], + 'plan' => $aref->[4], + }); + my($error); + $error=$part_bill_event->insert; + die $error if $error; + +} + +$dbh->commit or die $dbh->errstr; +$dbh->disconnect or die $dbh->errstr; + +#print "Freeside database initialized sucessfully\n"; + +sub usage { + die "Usage:\n freeside-setup [ -s ] user\n"; +} + +### +# Now it becomes an object. much better. +### +sub tables_hash_hack { + + #note that s/(date|change)/_$1/; to avoid keyword conflict. + #put a kludge in FS::Record to catch this or? (pry need some date-handling + #stuff anyway also) + + my(%tables)=( #yech.} + + 'agent' => { + 'columns' => [ + 'agentnum', 'serial', '', '', + 'agent', 'varchar', '', $char_d, + 'typenum', 'int', '', '', + 'freq', 'int', 'NULL', '', + 'prog', @perl_type, + 'disabled', 'char', 'NULL', 1, + 'username', 'varchar', 'NULL', $char_d, + '_password','varchar', 'NULL', $char_d, + ], + 'primary_key' => 'agentnum', + 'unique' => [], + 'index' => [ ['typenum'], ['disabled'] ], + }, + + 'agent_type' => { + 'columns' => [ + 'typenum', 'serial', '', '', + 'atype', 'varchar', '', $char_d, + ], + 'primary_key' => 'typenum', + 'unique' => [], + 'index' => [], + }, + + 'type_pkgs' => { + 'columns' => [ + 'typenum', 'int', '', '', + 'pkgpart', 'int', '', '', + ], + 'primary_key' => '', + 'unique' => [ ['typenum', 'pkgpart'] ], + 'index' => [ ['typenum'] ], + }, + + 'cust_bill' => { + 'columns' => [ + 'invnum', 'serial', '', '', + 'custnum', 'int', '', '', + '_date', @date_type, + 'charged', @money_type, + 'printed', 'int', '', '', + 'closed', 'char', 'NULL', 1, + ], + 'primary_key' => 'invnum', + 'unique' => [], + 'index' => [ ['custnum'], ['_date'] ], + }, + + 'cust_bill_event' => { + 'columns' => [ + 'eventnum', 'serial', '', '', + 'invnum', 'int', '', '', + 'eventpart', 'int', '', '', + '_date', @date_type, + 'status', 'varchar', '', $char_d, + 'statustext', 'text', 'NULL', '', + ], + 'primary_key' => 'eventnum', + #no... there are retries now #'unique' => [ [ 'eventpart', 'invnum' ] ], + 'unique' => [], + 'index' => [ ['invnum'], ['status'] ], + }, + + 'part_bill_event' => { + 'columns' => [ + 'eventpart', 'serial', '', '', + 'payby', 'char', '', 4, + 'event', 'varchar', '', $char_d, + 'eventcode', @perl_type, + 'seconds', 'int', 'NULL', '', + 'weight', 'int', '', '', + 'plan', 'varchar', 'NULL', $char_d, + 'plandata', 'text', 'NULL', '', + 'disabled', 'char', 'NULL', 1, + ], + 'primary_key' => 'eventpart', + 'unique' => [], + 'index' => [ ['payby'], ['disabled'], ], + }, + + 'cust_bill_pkg' => { + 'columns' => [ + 'pkgnum', 'int', '', '', + 'invnum', 'int', '', '', + 'setup', @money_type, + 'recur', @money_type, + 'sdate', @date_type, + 'edate', @date_type, + 'itemdesc', 'varchar', 'NULL', $char_d, + ], + 'primary_key' => '', + 'unique' => [], + 'index' => [ ['invnum'] ], + }, + + 'cust_bill_pkg_detail' => { + 'columns' => [ + 'detailnum', 'serial', '', '', + 'pkgnum', 'int', '', '', + 'invnum', 'int', '', '', + 'detail', 'varchar', '', $char_d, + ], + 'primary_key' => 'detailnum', + 'unique' => [], + 'index' => [ [ 'pkgnum', 'invnum' ] ], + }, + + 'cust_credit' => { + 'columns' => [ + 'crednum', 'serial', '', '', + 'custnum', 'int', '', '', + '_date', @date_type, + 'amount', @money_type, + 'otaker', 'varchar', '', 32, + 'reason', 'text', 'NULL', '', + 'closed', 'char', 'NULL', 1, + ], + 'primary_key' => 'crednum', + 'unique' => [], + 'index' => [ ['custnum'] ], + }, + + 'cust_credit_bill' => { + 'columns' => [ + 'creditbillnum', 'serial', '', '', + 'crednum', 'int', '', '', + 'invnum', 'int', '', '', + '_date', @date_type, + 'amount', @money_type, + ], + 'primary_key' => 'creditbillnum', + 'unique' => [], + 'index' => [ ['crednum'], ['invnum'] ], + }, + + 'cust_main' => { + 'columns' => [ + 'custnum', 'serial', '', '', + 'agentnum', 'int', '', '', +# 'titlenum', 'int', 'NULL', '', + 'last', 'varchar', '', $char_d, +# 'middle', 'varchar', 'NULL', $char_d, + 'first', 'varchar', '', $char_d, + 'ss', 'varchar', 'NULL', 11, + 'company', 'varchar', 'NULL', $char_d, + 'address1', 'varchar', '', $char_d, + 'address2', 'varchar', 'NULL', $char_d, + 'city', 'varchar', '', $char_d, + 'county', 'varchar', 'NULL', $char_d, + 'state', 'varchar', 'NULL', $char_d, + 'zip', 'varchar', '', 10, + 'country', 'char', '', 2, + 'daytime', 'varchar', 'NULL', 20, + 'night', 'varchar', 'NULL', 20, + 'fax', 'varchar', 'NULL', 12, + 'ship_last', 'varchar', 'NULL', $char_d, +# 'ship_middle', 'varchar', 'NULL', $char_d, + 'ship_first', 'varchar', 'NULL', $char_d, + 'ship_company', 'varchar', 'NULL', $char_d, + 'ship_address1', 'varchar', 'NULL', $char_d, + 'ship_address2', 'varchar', 'NULL', $char_d, + 'ship_city', 'varchar', 'NULL', $char_d, + 'ship_county', 'varchar', 'NULL', $char_d, + 'ship_state', 'varchar', 'NULL', $char_d, + 'ship_zip', 'varchar', 'NULL', 10, + 'ship_country', 'char', 'NULL', 2, + 'ship_daytime', 'varchar', 'NULL', 20, + 'ship_night', 'varchar', 'NULL', 20, + 'ship_fax', 'varchar', 'NULL', 12, + 'payby', 'char', '', 4, + 'payinfo', 'varchar', 'NULL', $char_d, + 'paycvv', 'varchar', 'NULL', 4, + #'paydate', @date_type, + 'paydate', 'varchar', 'NULL', 10, + 'payname', 'varchar', 'NULL', $char_d, + 'tax', 'char', 'NULL', 1, + 'otaker', 'varchar', '', 32, + 'refnum', 'int', '', '', + 'referral_custnum', 'int', 'NULL', '', + 'comments', 'text', 'NULL', '', + ], + 'primary_key' => 'custnum', + 'unique' => [], + #'index' => [ ['last'], ['company'] ], + 'index' => [ ['last'], [ 'company' ], [ 'referral_custnum' ], + [ 'daytime' ], [ 'night' ], [ 'fax' ], + ], + }, + + 'cust_main_invoice' => { + 'columns' => [ + 'destnum', 'serial', '', '', + 'custnum', 'int', '', '', + 'dest', 'varchar', '', $char_d, + ], + 'primary_key' => 'destnum', + 'unique' => [], + 'index' => [ ['custnum'], ], + }, + + 'cust_main_county' => { #county+state+country are checked off the + #cust_main_county for validation and to provide + # a tax rate. + 'columns' => [ + 'taxnum', 'serial', '', '', + 'state', 'varchar', 'NULL', $char_d, + 'county', 'varchar', 'NULL', $char_d, + 'country', 'char', '', 2, + 'taxclass', 'varchar', 'NULL', $char_d, + 'exempt_amount', @money_type, + 'tax', 'real', '', '', #tax % + 'taxname', 'varchar', 'NULL', $char_d, + 'setuptax', 'char', 'NULL', 1, # Y = setup tax exempt + 'recurtax', 'char', 'NULL', 1, # Y = recur tax exempt + ], + 'primary_key' => 'taxnum', + 'unique' => [], + # 'unique' => [ ['taxnum'], ['state', 'county'] ], + 'index' => [], + }, + + 'cust_pay' => { + 'columns' => [ + 'paynum', 'serial', '', '', + #now cust_bill_pay #'invnum', 'int', '', '', + 'custnum', 'int', '', '', + 'paid', @money_type, + '_date', @date_type, + 'payby', 'char', '', 4, # CARD/BILL/COMP, should be index into + # payment type table. + 'payinfo', 'varchar', 'NULL', $char_d, #see cust_main above + 'paybatch', 'varchar', 'NULL', $char_d, #for auditing purposes. + 'closed', 'char', 'NULL', 1, + ], + 'primary_key' => 'paynum', + 'unique' => [], + 'index' => [ [ 'custnum' ], [ 'paybatch' ], [ 'payby' ], [ '_date' ] ], + }, + + 'cust_bill_pay' => { + 'columns' => [ + 'billpaynum', 'serial', '', '', + 'invnum', 'int', '', '', + 'paynum', 'int', '', '', + 'amount', @money_type, + '_date', @date_type + ], + 'primary_key' => 'billpaynum', + 'unique' => [], + 'index' => [ [ 'paynum' ], [ 'invnum' ] ], + }, + + 'cust_pay_batch' => { #what's this used for again? list of customers + #in current CARD batch? (necessarily CARD?) + 'columns' => [ + 'paybatchnum', 'serial', '', '', + 'invnum', 'int', '', '', + 'custnum', 'int', '', '', + 'last', 'varchar', '', $char_d, + 'first', 'varchar', '', $char_d, + 'address1', 'varchar', '', $char_d, + 'address2', 'varchar', 'NULL', $char_d, + 'city', 'varchar', '', $char_d, + 'state', 'varchar', 'NULL', $char_d, + 'zip', 'varchar', '', 10, + 'country', 'char', '', 2, +# 'trancode', 'int', '', '', + 'cardnum', 'varchar', '', 16, + #'exp', @date_type, + 'exp', 'varchar', '', 11, + 'payname', 'varchar', 'NULL', $char_d, + 'amount', @money_type, + ], + 'primary_key' => 'paybatchnum', + 'unique' => [], + 'index' => [ ['invnum'], ['custnum'] ], + }, + + 'cust_pkg' => { + 'columns' => [ + 'pkgnum', 'serial', '', '', + 'custnum', 'int', '', '', + 'pkgpart', 'int', '', '', + 'otaker', 'varchar', '', 32, + 'setup', @date_type, + 'bill', @date_type, + 'last_bill', @date_type, + 'susp', @date_type, + 'cancel', @date_type, + 'expire', @date_type, + 'manual_flag', 'char', 'NULL', 1, + ], + 'primary_key' => 'pkgnum', + 'unique' => [], + 'index' => [ ['custnum'] ], + }, + + 'cust_refund' => { + 'columns' => [ + 'refundnum', 'serial', '', '', + #now cust_credit_refund #'crednum', 'int', '', '', + 'custnum', 'int', '', '', + '_date', @date_type, + 'refund', @money_type, + 'otaker', 'varchar', '', 32, + 'reason', 'varchar', '', $char_d, + 'payby', 'char', '', 4, # CARD/BILL/COMP, should be index + # into payment type table. + 'payinfo', 'varchar', 'NULL', $char_d, #see cust_main above + 'paybatch', 'varchar', 'NULL', $char_d, + 'closed', 'char', 'NULL', 1, + ], + 'primary_key' => 'refundnum', + 'unique' => [], + 'index' => [], + }, + + 'cust_credit_refund' => { + 'columns' => [ + 'creditrefundnum', 'serial', '', '', + 'crednum', 'int', '', '', + 'refundnum', 'int', '', '', + 'amount', @money_type, + '_date', @date_type + ], + 'primary_key' => 'creditrefundnum', + 'unique' => [], + 'index' => [ [ 'crednum', 'refundnum' ] ], + }, + + + 'cust_svc' => { + 'columns' => [ + 'svcnum', 'serial', '', '', + 'pkgnum', 'int', 'NULL', '', + 'svcpart', 'int', '', '', + ], + 'primary_key' => 'svcnum', + 'unique' => [], + 'index' => [ ['svcnum'], ['pkgnum'], ['svcpart'] ], + }, + + 'part_pkg' => { + 'columns' => [ + 'pkgpart', 'serial', '', '', + 'pkg', 'varchar', '', $char_d, + 'comment', 'varchar', '', $char_d, + 'setup', @perl_type, + 'freq', 'varchar', '', $char_d, #billing frequency + 'recur', @perl_type, + 'setuptax', 'char', 'NULL', 1, + 'recurtax', 'char', 'NULL', 1, + 'plan', 'varchar', 'NULL', $char_d, + 'plandata', 'text', 'NULL', '', + 'disabled', 'char', 'NULL', 1, + 'taxclass', 'varchar', 'NULL', $char_d, + ], + 'primary_key' => 'pkgpart', + 'unique' => [], + 'index' => [ [ 'disabled' ], ], + }, + +# 'part_title' => { +# 'columns' => [ +# 'titlenum', 'int', '', '', +# 'title', 'varchar', '', $char_d, +# ], +# 'primary_key' => 'titlenum', +# 'unique' => [ [] ], +# 'index' => [ [] ], +# }, + + 'pkg_svc' => { + 'columns' => [ + 'pkgpart', 'int', '', '', + 'svcpart', 'int', '', '', + 'quantity', 'int', '', '', + 'primary_svc','char', 'NULL', 1, + ], + 'primary_key' => '', + 'unique' => [ ['pkgpart', 'svcpart'] ], + 'index' => [ ['pkgpart'] ], + }, + + 'part_referral' => { + 'columns' => [ + 'refnum', 'serial', '', '', + 'referral', 'varchar', '', $char_d, + 'disabled', 'char', 'NULL', 1, + ], + 'primary_key' => 'refnum', + 'unique' => [], + 'index' => [ ['disabled'] ], + }, + + 'part_svc' => { + 'columns' => [ + 'svcpart', 'serial', '', '', + 'svc', 'varchar', '', $char_d, + 'svcdb', 'varchar', '', $char_d, + 'disabled', 'char', 'NULL', 1, + ], + 'primary_key' => 'svcpart', + 'unique' => [], + 'index' => [ [ 'disabled' ] ], + }, + + 'part_svc_column' => { + 'columns' => [ + 'columnnum', 'serial', '', '', + 'svcpart', 'int', '', '', + 'columnname', 'varchar', '', 64, + 'columnvalue', 'varchar', 'NULL', $char_d, + 'columnflag', 'char', 'NULL', 1, + ], + 'primary_key' => 'columnnum', + 'unique' => [ [ 'svcpart', 'columnname' ] ], + 'index' => [ [ 'svcpart' ] ], + }, + + #(this should be renamed to part_pop) + 'svc_acct_pop' => { + 'columns' => [ + 'popnum', 'serial', '', '', + 'city', 'varchar', '', $char_d, + 'state', 'varchar', '', $char_d, + 'ac', 'char', '', 3, + 'exch', 'char', '', 3, + 'loc', 'char', 'NULL', 4, #NULL for legacy purposes + ], + 'primary_key' => 'popnum', + 'unique' => [], + 'index' => [ [ 'state' ] ], + }, + + 'part_pop_local' => { + 'columns' => [ + 'localnum', 'serial', '', '', + 'popnum', 'int', '', '', + 'city', 'varchar', 'NULL', $char_d, + 'state', 'char', 'NULL', 2, + 'npa', 'char', '', 3, + 'nxx', 'char', '', 3, + ], + 'primary_key' => 'localnum', + 'unique' => [], + 'index' => [ [ 'npa', 'nxx' ], [ 'popnum' ] ], + }, + + 'svc_acct' => { + 'columns' => [ + 'svcnum', 'int', '', '', + 'username', 'varchar', '', $username_len, #unique (& remove dup code) + '_password', 'varchar', '', 72, #13 for encryped pw's plus ' *SUSPENDED* (md5 passwords can be 34, blowfish 60) + 'sec_phrase', 'varchar', 'NULL', $char_d, + 'popnum', 'int', 'NULL', '', + 'uid', 'int', 'NULL', '', + 'gid', 'int', 'NULL', '', + 'finger', 'varchar', 'NULL', $char_d, + 'dir', 'varchar', 'NULL', $char_d, + 'shell', 'varchar', 'NULL', $char_d, + 'quota', 'varchar', 'NULL', $char_d, + 'slipip', 'varchar', 'NULL', 15, #four TINYINTs, bah. + 'seconds', 'int', 'NULL', '', #uhhhh + 'domsvc', 'int', '', '', + ], + 'primary_key' => 'svcnum', + #'unique' => [ [ 'username', 'domsvc' ] ], + 'unique' => [], + 'index' => [ ['username'], ['domsvc'] ], + }, + + #'svc_charge' => { + # 'columns' => [ + # 'svcnum', 'int', '', '', + # 'amount', @money_type, + # ], + # 'primary_key' => 'svcnum', + # 'unique' => [ [] ], + # 'index' => [ [] ], + #}, + + 'svc_domain' => { + 'columns' => [ + 'svcnum', 'int', '', '', + 'domain', 'varchar', '', $char_d, + 'catchall', 'int', 'NULL', '', + ], + 'primary_key' => 'svcnum', + 'unique' => [ ['domain'] ], + 'index' => [], + }, + + 'domain_record' => { + 'columns' => [ + 'recnum', 'serial', '', '', + 'svcnum', 'int', '', '', + #'reczone', 'varchar', '', $char_d, + 'reczone', 'varchar', '', 255, + 'recaf', 'char', '', 2, + 'rectype', 'varchar', '', 5, + #'recdata', 'varchar', '', $char_d, + 'recdata', 'varchar', '', 255, + ], + 'primary_key' => 'recnum', + 'unique' => [], + 'index' => [ ['svcnum'] ], + }, + + 'svc_forward' => { + 'columns' => [ + 'svcnum', 'int', '', '', + 'srcsvc', 'int', 'NULL', '', + 'src', 'varchar', 'NULL', 255, + 'dstsvc', 'int', 'NULL', '', + 'dst', 'varchar', 'NULL', 255, + ], + 'primary_key' => 'svcnum', + 'unique' => [], + 'index' => [ ['srcsvc'], ['dstsvc'] ], + }, + + 'svc_www' => { + 'columns' => [ + 'svcnum', 'int', '', '', + 'recnum', 'int', '', '', + 'usersvc', 'int', '', '', + ], + 'primary_key' => 'svcnum', + 'unique' => [], + 'index' => [], + }, + + #'svc_wo' => { + # 'columns' => [ + # 'svcnum', 'int', '', '', + # 'svcnum', 'int', '', '', + # 'svcnum', 'int', '', '', + # 'worker', 'varchar', '', $char_d, + # '_date', @date_type, + # ], + # 'primary_key' => 'svcnum', + # 'unique' => [ [] ], + # 'index' => [ [] ], + #}, + + 'prepay_credit' => { + 'columns' => [ + 'prepaynum', 'serial', '', '', + 'identifier', 'varchar', '', $char_d, + 'amount', @money_type, + 'seconds', 'int', 'NULL', '', + ], + 'primary_key' => 'prepaynum', + 'unique' => [ ['identifier'] ], + 'index' => [], + }, + + 'port' => { + 'columns' => [ + 'portnum', 'serial', '', '', + 'ip', 'varchar', 'NULL', 15, + 'nasport', 'int', 'NULL', '', + 'nasnum', 'int', '', '', + ], + 'primary_key' => 'portnum', + 'unique' => [], + 'index' => [], + }, + + 'nas' => { + 'columns' => [ + 'nasnum', 'serial', '', '', + 'nas', 'varchar', '', $char_d, + 'nasip', 'varchar', '', 15, + 'nasfqdn', 'varchar', '', $char_d, + 'last', 'int', '', '', + ], + 'primary_key' => 'nasnum', + 'unique' => [ [ 'nas' ], [ 'nasip' ] ], + 'index' => [ [ 'last' ] ], + }, + + 'session' => { + 'columns' => [ + 'sessionnum', 'serial', '', '', + 'portnum', 'int', '', '', + 'svcnum', 'int', '', '', + 'login', @date_type, + 'logout', @date_type, + ], + 'primary_key' => 'sessionnum', + 'unique' => [], + 'index' => [ [ 'portnum' ] ], + }, + + 'queue' => { + 'columns' => [ + 'jobnum', 'serial', '', '', + 'job', 'text', '', '', + '_date', 'int', '', '', + 'status', 'varchar', '', $char_d, + 'statustext', 'text', 'NULL', '', + 'svcnum', 'int', 'NULL', '', + ], + 'primary_key' => 'jobnum', + 'unique' => [], + 'index' => [ [ 'svcnum' ], [ 'status' ] ], + }, + + 'queue_arg' => { + 'columns' => [ + 'argnum', 'serial', '', '', + 'jobnum', 'int', '', '', + 'arg', 'text', 'NULL', '', + ], + 'primary_key' => 'argnum', + 'unique' => [], + 'index' => [ [ 'jobnum' ] ], + }, + + 'queue_depend' => { + 'columns' => [ + 'dependnum', 'serial', '', '', + 'jobnum', 'int', '', '', + 'depend_jobnum', 'int', '', '', + ], + 'primary_key' => 'dependnum', + 'unique' => [], + 'index' => [ [ 'jobnum' ], [ 'depend_jobnum' ] ], + }, + + 'export_svc' => { + 'columns' => [ + 'exportsvcnum' => 'serial', '', '', + 'exportnum' => 'int', '', '', + 'svcpart' => 'int', '', '', + ], + 'primary_key' => 'exportsvcnum', + 'unique' => [ [ 'exportnum', 'svcpart' ] ], + 'index' => [ [ 'exportnum' ], [ 'svcpart' ] ], + }, + + 'part_export' => { + 'columns' => [ + 'exportnum', 'serial', '', '', + #'svcpart', 'int', '', '', + 'machine', 'varchar', '', $char_d, + 'exporttype', 'varchar', '', $char_d, + 'nodomain', 'char', 'NULL', 1, + ], + 'primary_key' => 'exportnum', + 'unique' => [], + 'index' => [ [ 'machine' ], [ 'exporttype' ] ], + }, + + 'part_export_option' => { + 'columns' => [ + 'optionnum', 'serial', '', '', + 'exportnum', 'int', '', '', + 'optionname', 'varchar', '', $char_d, + 'optionvalue', 'text', 'NULL', '', + ], + 'primary_key' => 'optionnum', + 'unique' => [], + 'index' => [ [ 'exportnum' ], [ 'optionname' ] ], + }, + + 'radius_usergroup' => { + 'columns' => [ + 'usergroupnum', 'serial', '', '', + 'svcnum', 'int', '', '', + 'groupname', 'varchar', '', $char_d, + ], + 'primary_key' => 'usergroupnum', + 'unique' => [], + 'index' => [ [ 'svcnum' ], [ 'groupname' ] ], + }, + + 'msgcat' => { + 'columns' => [ + 'msgnum', 'serial', '', '', + 'msgcode', 'varchar', '', $char_d, + 'locale', 'varchar', '', 16, + 'msg', 'text', '', '', + ], + 'primary_key' => 'msgnum', + 'unique' => [ [ 'msgcode', 'locale' ] ], + 'index' => [], + }, + + 'cust_tax_exempt' => { + 'columns' => [ + 'exemptnum', 'serial', '', '', + 'custnum', 'int', '', '', + 'taxnum', 'int', '', '', + 'year', 'int', '', '', + 'month', 'int', '', '', + 'amount', @money_type, + ], + 'primary_key' => 'exemptnum', + 'unique' => [ [ 'custnum', 'taxnum', 'year', 'month' ] ], + 'index' => [], + }, + + 'router' => { + 'columns' => [ + 'routernum', 'serial', '', '', + 'routername', 'varchar', '', $char_d, + 'svcnum', 'int', 'NULL', '', + ], + 'primary_key' => 'routernum', + 'unique' => [], + 'index' => [], + }, + + 'part_svc_router' => { + 'columns' => [ + 'svcpart', 'int', '', '', + 'routernum', 'int', '', '', + ], + 'primary_key' => '', + 'unique' => [], + 'index' => [], + }, + + 'addr_block' => { + 'columns' => [ + 'blocknum', 'serial', '', '', + 'routernum', 'int', '', '', + 'ip_gateway', 'varchar', '', 15, + 'ip_netmask', 'int', '', '', + ], + 'primary_key' => 'blocknum', + 'unique' => [ [ 'blocknum', 'routernum' ] ], + 'index' => [], + }, + + 'svc_broadband' => { + 'columns' => [ + 'svcnum', 'int', '', '', + 'blocknum', 'int', '', '', + 'speed_up', 'int', '', '', + 'speed_down', 'int', '', '', + 'ip_addr', 'varchar', '', 15, + ], + 'primary_key' => 'svcnum', + 'unique' => [], + 'index' => [], + }, + + 'part_virtual_field' => { + 'columns' => [ + 'vfieldpart', 'int', '', '', + 'dbtable', 'varchar', '', 32, + 'name', 'varchar', '', 32, + 'check_block', 'text', 'NULL', '', + 'length', 'int', 'NULL', '', + 'list_source', 'text', 'NULL', '', + 'label', 'varchar', 'NULL', 80, + ], + 'primary_key' => 'vfieldpart', + 'unique' => [], + 'index' => [], + }, + + 'virtual_field' => { + 'columns' => [ + 'recnum', 'int', '', '', + 'vfieldpart', 'int', '', '', + 'value', 'varchar', '', 128, + ], + 'primary_key' => '', + 'unique' => [ [ 'vfieldpart', 'recnum' ] ], + 'index' => [], + }, + + 'acct_snarf' => { + 'columns' => [ + 'snarfnum', 'int', '', '', + 'svcnum', 'int', '', '', + 'machine', 'varchar', '', 255, + 'protocol', 'varchar', '', $char_d, + 'username', 'varchar', '', $char_d, + '_password', 'varchar', '', $char_d, + ], + 'primary_key' => 'snarfnum', + 'unique' => [], + 'index' => [ [ 'svcnum' ] ], + }, + + 'svc_external' => { + 'columns' => [ + 'svcnum', 'int', '', '', + 'id', 'int', '', '', + 'title', 'varchar', 'NULL', $char_d, + ], + 'primary_key' => 'svcnum', + 'unique' => [], + 'index' => [], + }, + + ); + + %tables; + +} + diff --git a/FS/bin/freeside-sqlradius-radacctd b/FS/bin/freeside-sqlradius-radacctd new file mode 100644 index 000000000..4e8d57c51 --- /dev/null +++ b/FS/bin/freeside-sqlradius-radacctd @@ -0,0 +1,180 @@ +#!/usr/bin/perl -Tw + +use strict; +use vars qw( $log_file $sigterm $sigint ); +use subs qw( _die _logmsg ); +use Fcntl qw(:flock); +use POSIX qw(setsid); +use Date::Format; +use IO::File; +use FS::UID qw(adminsuidsetup); +#use FS::Record qw(qsearch qsearchs); +#use FS::part_export; +#use FS::svc_acct; +#use FS::cust_svc; + +#lots of false laziness w/freeside-queued + +my $user = shift or die &usage; + +#my $pid_file = "/var/run/freeside-sqlradius-radacctd.$user.pid"; +my $pid_file = "/var/run/freeside-sqlradius-radacctd.pid"; + +&daemonize1; + +#sub REAPER { my $pid = wait; $SIG{CHLD} = \&REAPER; $kids--; } +#$SIG{CHLD} = \&REAPER; + +$sigterm = 0; +$sigint = 0; +$SIG{INT} = sub { warn "SIGINT received; shutting down\n"; $sigint++; }; +$SIG{TERM} = sub { warn "SIGTERM received; shutting down\n"; $sigterm++; }; + +my $freeside_gid = scalar(getgrnam('freeside')) + or die "can't setgid to freeside group\n"; +$) = $freeside_gid; +$( = $freeside_gid; +#if freebsd can't setuid(), presumably it can't setgid() either. grr fleabsd +($(,$)) = ($),$(); +$) = $freeside_gid; + +$> = $FS::UID::freeside_uid; +$< = $FS::UID::freeside_uid; +#freebsd is sofa king broken, won't setuid() +($<,$>) = ($>,$<); +$> = $FS::UID::freeside_uid; + +#$ENV{HOME} = (getpwuid($>))[7]; #for ssh +adminsuidsetup $user; + +$log_file= "/usr/local/etc/freeside/sqlradius-radacctd-log.". $FS::UID::datasrc; + +&daemonize2; + +$SIG{__DIE__} = \&_die; +$SIG{__WARN__} = \&_logmsg; + +warn "freeside-sqlradius-radacctd starting\n"; + +#eslaf + +#my $machine = shift or die &usage; #would need to be up higher for real +my @exports = qsearch('part_export', { 'exporttype' => 'sqlradius' } ); + +while (1) { + + my %seen = (); + foreach my $export ( @exports ) { + next if $seen{$export->option('datasrc')}++; + my $dbh = DBI->connect( + map { $export->option($_) } qw( datasrc username password ) + ) or do { + warn "can't connect to ". $export->option('datasrc'). ": ". $DBI::errstr; + next; + } + + # find old radacct position + #$lastid = 0; + + # get new radacct records + my $sth = $dbh->prepare('SELECT * FROM radacct WHERE radacctid > ?') or do { + warn "can't select in radacct table from ". $export->option('datasrc'). + ": ". $dbh->errstr; + next; + }; + + while ( my $radacct = $sth->fetchrow_arrayref({}) ) { + + my $session = new FS::session { + portnum => + svcnum => + login => + #logout => + }; + + } + + # look for updated radacct records & replace them + + } + + sleep 5; + +} + +#more false laziness w/freeside-queued + +sub usage { + die "Usage:\n\n freeside-sqlradius-radacctd user\n"; +} + +sub _die { + my $msg = shift; + unlink $pid_file if -e $pid_file; + _logmsg($msg); +} + +sub _logmsg { + chomp( my $msg = shift ); + my $log = new IO::File ">>$log_file"; + flock($log, LOCK_EX); + seek($log, 0, 2); + print $log "[". time2str("%a %b %e %T %Y",time). "] [$$] $msg\n"; + flock($log, LOCK_UN); + close $log; +} + +sub daemonize1 { + + chdir "/" or die "Can't chdir to /: $!"; + open STDIN, '/dev/null' or die "Can't read /dev/null: $!"; + defined(my $pid = fork) or die "Can't fork: $!"; + if ( $pid ) { + print "freeside-sqlradius-radacctd started with pid $pid\n"; + #logging to $log_file\n"; + exit unless $pid_file; + my $pidfh = new IO::File ">$pid_file" or exit; + print $pidfh "$pid\n"; + exit; + } + #open STDOUT, '>/dev/null' + # or die "Can't write to /dev/null: $!"; + #setsid or die "Can't start a new session: $!"; + #open STDERR, '>&STDOUT' or die "Can't dup stdout: $!"; + +} + +sub daemonize2 { + open STDOUT, '>/dev/null' + or die "Can't write to /dev/null: $!"; + setsid or die "Can't start a new session: $!"; + open STDERR, '>&STDOUT' or die "Can't dup stdout: $!"; +} + + +#eslaf + +=head1 NAME + +freeside-sqlradius-radacctd - Real-time radacct import daemon + +=head1 SYNOPSIS + + freeside-sqlradius-radacctd username + +=head1 DESCRIPTION + +Imports records from an SQL radacct table in real-time into the session +monitor. + +This enables per-minute or per-hour charges as well as the +"View active NAS ports" function. + +B<username> is a username added by freeside-adduser. + +=head1 SEE ALSO + +session.html from the base documentation. + +=cut + diff --git a/FS/bin/freeside-sqlradius-reset b/FS/bin/freeside-sqlradius-reset index 9d3a6a700..74f90a582 100755 --- a/FS/bin/freeside-sqlradius-reset +++ b/FS/bin/freeside-sqlradius-reset @@ -12,7 +12,9 @@ adminsuidsetup $user; #my $machine = shift or die &usage; -my @exports = qsearch('part_export', { 'exporttype' => 'sqlradius' } ); +my @exports = qsearch('part_export', { exporttype=>'sqlradius' } ); +push @exports, qsearch('part_export', { exporttype=>'sqlradius_withdomain' } ); + foreach my $export ( @exports ) { my $icradius_dbh = DBI->connect( diff --git a/FS/bin/freeside-sqlradius-seconds b/FS/bin/freeside-sqlradius-seconds new file mode 100644 index 000000000..1c978fa8a --- /dev/null +++ b/FS/bin/freeside-sqlradius-seconds @@ -0,0 +1,58 @@ +#!/usr/bin/perl -Tw + +use strict; +use Date::Parse; +use FS::UID qw(adminsuidsetup); +use FS::Record qw(qsearchs); +use FS::svc_acct; + +my $fs_user = shift or die &usage; +adminsuidsetup( $fs_user ); + +my $target_user = shift or die &usage; +my $start = shift or die &usage; +$start = str2time($start); +my $stop = scalar(@ARGV) ? str2time(shift) : time; + +my $svc_acct = qsearchs( 'svc_acct', { 'username' => $target_user } ); +die "username $target_user not found\n" unless $svc_acct; + +print $svc_acct->seconds_since_sqlradacct( $start, $stop ). "\n"; + +sub usage { + die "Usage:\n\n freeside-sqlradius-seconds freeside_username target_username start_date stop_date\n"; +} + + +=head1 NAME + +freeside-sqlradius-seconds - Real-time radacct import daemon + +=head1 SYNOPSIS + + freeside-sqlradius-seconds freeside_username target_username start_date [ stop_date ] + +=head1 DESCRIPTION + +Returns the number of seconds the specified username has been online between +start_date (inclusive) and stop_date (exclusive). +See L<FS::svc_acct/seconds_since_sqlradacct> + +B<freeside_username> is a username added by freeside-adduser. +B<target_username> is the username of the user account to query. +B<start_date> and B<stop_date> are in any format Date::Parse is happy with. +B<stop_date> defaults to now if not specified. + +=head1 BUGS + +Selection of the account in question is rather simplistic in that +B<target_username> doesn't necessarily identify a unique account (and wouldn't +even if a domain was specified), and no sqlradius export is checked for. + +=head1 SEE ALSO + +L<FS::svc_acct/seconds_since_sqlradacct> + +=cut + +1; diff --git a/FS/bin/freeside-tax-report b/FS/bin/freeside-tax-report index 8d5021358..240f3ad37 100755 --- a/FS/bin/freeside-tax-report +++ b/FS/bin/freeside-tax-report @@ -228,7 +228,7 @@ if($email && $opt_m) # subroutines sub untaint_argv { foreach $_ ( $[ .. $#ARGV ) { #untaint @ARGV - $ARGV[$_] =~ /^([\w\-\/ :]*)$/ || die "Illegal argument \"$ARGV[$_]\""; + $ARGV[$_] =~ /^([\w\-\/ :\.]*)$/ || die "Illegal argument \"$ARGV[$_]\""; $ARGV[$_]=$1; } } @@ -267,7 +267,7 @@ user: From the mapsecrets file - see config.html from the base documentation =head1 VERSION -$Id: freeside-tax-report,v 1.4 2002-03-07 19:50:24 jeff Exp $ +$Id: freeside-tax-report,v 1.5 2002-09-09 22:57:34 ivan Exp $ =head1 BUGS diff --git a/FS/t/svc_acct_sm.t b/FS/t/Misc.t index 1082f2cdb..cc7751ab6 100644 --- a/FS/t/svc_acct_sm.t +++ b/FS/t/Misc.t @@ -1,5 +1,5 @@ BEGIN { $| = 1; print "1..1\n" } END {print "not ok 1\n" unless $loaded;} -use FS::svc_acct_sm; +use FS::Misc; $loaded=1; print "ok 1\n"; diff --git a/FS/t/acct_snarf.t b/FS/t/acct_snarf.t new file mode 100644 index 000000000..642760f20 --- /dev/null +++ b/FS/t/acct_snarf.t @@ -0,0 +1,5 @@ +BEGIN { $| = 1; print "1..1\n" } +END {print "not ok 1\n" unless $loaded;} +use FS::acct_snarf; +$loaded=1; +print "ok 1\n"; diff --git a/FS/t/cust_bill_pkg_detail.t b/FS/t/cust_bill_pkg_detail.t new file mode 100644 index 000000000..ea6e3d125 --- /dev/null +++ b/FS/t/cust_bill_pkg_detail.t @@ -0,0 +1,5 @@ +BEGIN { $| = 1; print "1..1\n" } +END {print "not ok 1\n" unless $loaded;} +use FS::cust_bill_pkg_detail; +$loaded=1; +print "ok 1\n"; diff --git a/FS/t/part_export-apache.t b/FS/t/part_export-apache.t new file mode 100644 index 000000000..b9995080f --- /dev/null +++ b/FS/t/part_export-apache.t @@ -0,0 +1,5 @@ +BEGIN { $| = 1; print "1..1\n" } +END {print "not ok 1\n" unless $loaded;} +use FS::part_export::apache; +$loaded=1; +print "ok 1\n"; diff --git a/FS/t/part_export-domain_shellcommands.t b/FS/t/part_export-domain_shellcommands.t new file mode 100644 index 000000000..a2a44fbfb --- /dev/null +++ b/FS/t/part_export-domain_shellcommands.t @@ -0,0 +1,5 @@ +BEGIN { $| = 1; print "1..1\n" } +END {print "not ok 1\n" unless $loaded;} +use FS::part_export::domain_shellcommands; +$loaded=1; +print "ok 1\n"; diff --git a/FS/t/part_export-forward_shellcommands.t b/FS/t/part_export-forward_shellcommands.t new file mode 100644 index 000000000..78ca68d10 --- /dev/null +++ b/FS/t/part_export-forward_shellcommands.t @@ -0,0 +1,5 @@ +BEGIN { $| = 1; print "1..1\n" } +END {print "not ok 1\n" unless $loaded;} +use FS::part_export::forward_shellcommands; +$loaded=1; +print "ok 1\n"; diff --git a/FS/t/part_export-ldap.t b/FS/t/part_export-ldap.t new file mode 100644 index 000000000..826c3418d --- /dev/null +++ b/FS/t/part_export-ldap.t @@ -0,0 +1,5 @@ +BEGIN { $| = 1; print "1..1\n" } +END {print "not ok 1\n" unless $loaded;} +use FS::part_export::ldap; +$loaded=1; +print "ok 1\n"; diff --git a/FS/t/part_export-sqlradius_withdomain.t b/FS/t/part_export-sqlradius_withdomain.t new file mode 100644 index 000000000..504bf679f --- /dev/null +++ b/FS/t/part_export-sqlradius_withdomain.t @@ -0,0 +1,5 @@ +BEGIN { $| = 1; print "1..1\n" } +END {print "not ok 1\n" unless $loaded;} +use FS::part_export::sqlradius_withdomain; +$loaded=1; +print "ok 1\n"; diff --git a/FS/t/svc_broadband.t b/FS/t/svc_broadband.t new file mode 100644 index 000000000..02dc1124a --- /dev/null +++ b/FS/t/svc_broadband.t @@ -0,0 +1,5 @@ +BEGIN { $| = 1; print "1..1\n" } +END {print "not ok 1\n" unless $loaded;} +use FS::svc_broadband; +$loaded=1; +print "ok 1\n"; diff --git a/FS/t/svc_external.t b/FS/t/svc_external.t new file mode 100644 index 000000000..20a676784 --- /dev/null +++ b/FS/t/svc_external.t @@ -0,0 +1,5 @@ +BEGIN { $| = 1; print "1..1\n" } +END {print "not ok 1\n" unless $loaded;} +use FS::svc_external; +$loaded=1; +print "ok 1\n"; |