summaryrefslogtreecommitdiff
path: root/FS/bin
diff options
context:
space:
mode:
Diffstat (limited to 'FS/bin')
-rwxr-xr-xFS/bin/freeside-addgroup50
-rw-r--r--FS/bin/freeside-addoutsource32
-rw-r--r--FS/bin/freeside-addoutsourceuser18
-rw-r--r--FS/bin/freeside-adduser119
-rwxr-xr-xFS/bin/freeside-apply-credits21
-rwxr-xr-xFS/bin/freeside-count-active-customers17
-rwxr-xr-xFS/bin/freeside-daily92
-rw-r--r--FS/bin/freeside-deloutsource14
-rw-r--r--FS/bin/freeside-deloutsourceuser6
-rw-r--r--FS/bin/freeside-deluser64
-rwxr-xr-xFS/bin/freeside-email55
-rwxr-xr-xFS/bin/freeside-expiration-alerter222
-rwxr-xr-xFS/bin/freeside-monthly91
-rw-r--r--FS/bin/freeside-prepaidd104
-rw-r--r--FS/bin/freeside-queued250
-rw-r--r--FS/bin/freeside-radgroup76
-rw-r--r--FS/bin/freeside-reexport71
-rwxr-xr-xFS/bin/freeside-reset-fixed69
-rw-r--r--FS/bin/freeside-selfservice-server229
-rw-r--r--FS/bin/freeside-setinvoice42
-rwxr-xr-xFS/bin/freeside-setup130
-rw-r--r--FS/bin/freeside-sqlradius-radacctd150
-rwxr-xr-xFS/bin/freeside-sqlradius-reset88
-rw-r--r--FS/bin/freeside-sqlradius-seconds58
-rwxr-xr-xFS/bin/freeside-upgrade85
25 files changed, 2153 insertions, 0 deletions
diff --git a/FS/bin/freeside-addgroup b/FS/bin/freeside-addgroup
new file mode 100755
index 000000000..7b30f7d95
--- /dev/null
+++ b/FS/bin/freeside-addgroup
@@ -0,0 +1,50 @@
+#!/usr/bin/perl
+
+use strict;
+use vars qw($opt_s);
+use Getopt::Std;
+use FS::UID qw(adminsuidsetup);
+use FS::Record qw(qsearch);
+use FS::CurrentUser;
+use FS::AccessRight;
+use FS::access_group;
+use FS::access_right;
+use FS::access_groupagent;
+
+getopts("s");
+my $user = shift or die &usage; #just for adminsuidsetup
+my $group = shift or die &usage;
+
+$FS::CurrentUser::upgrade_hack = 1;
+#adminsuidsetup $rootuser;
+adminsuidsetup $user;
+
+my $access_group = new FS::access_group { 'groupname' => $group };
+my $error = $access_group->insert;
+die $error if $error;
+
+if ( $opt_s ) {
+ foreach my $rightname ( FS::AccessRight->rights ) {
+ my $access_right = new FS::access_right {
+ 'righttype' => 'FS::access_group',
+ 'rightobjnum' => $access_group->groupnum,
+ 'rightname' => $rightname,
+ };
+ my $ar_error = $access_right->insert;
+ die $ar_error if $ar_error;
+ }
+
+ foreach my $agent ( qsearch('agent', {} ) ) {
+ my $access_groupagent = new FS::access_groupagent {
+ 'groupnum' => $access_group->groupnum,
+ 'agentnum' => $agent->agentnum,
+ };
+ my $aga_error = $access_groupagent->insert;
+ die $aga_error if $aga_error;
+ }
+}
+
+sub usage {
+ die "Usage:\n\n freeside-addgroup [ -s ] username groupname"
+}
+
diff --git a/FS/bin/freeside-addoutsource b/FS/bin/freeside-addoutsource
new file mode 100644
index 000000000..9cb12195a
--- /dev/null
+++ b/FS/bin/freeside-addoutsource
@@ -0,0 +1,32 @@
+#!/bin/sh
+
+domain=$1
+
+FREESIDE_CONF=%%%FREESIDE_CONF%%%
+FREESIDE_CACHE=%%%FREESIDE_CACHE%%%
+FREESIDE_EXPORT=%%%FREESIDE_EXPORT%%%
+
+#without this, [a-z]* matches CVS/, the copy doesn't return a sucessful error
+# status, and the rest of the commands aren't run
+export LANG=C
+
+createdb $domain && \
+\
+mkdir $FREESIDE_CONF/conf.DBI:Pg:dbname=$domain && \
+\
+chown freeside $FREESIDE_CONF/conf.DBI:Pg:dbname=$domain && \
+\
+cp /home/ivan/freeside/conf/[a-z]* $FREESIDE_CONF/conf.DBI:Pg:dbname=$domain && \
+\
+touch $FREESIDE_CONF/conf.DBI:Pg:dbname=$domain/secrets && \
+\
+chown freeside $FREESIDE_CONF/conf.DBI:Pg:dbname=$domain/secrets && \
+\
+chmod 600 $FREESIDE_CONF/conf.DBI:Pg:dbname=$domain/secrets && \
+\
+echo -e "DBI:Pg:dbname=$domain\nfreeside\n" >$FREESIDE_CONF/conf.DBI:Pg:dbname=$domain/secrets && \
+\
+mkdir $FREESIDE_CACHE/counters.DBI:Pg:dbname=$domain && \
+mkdir $FREESIDE_CACHE/cache.DBI:Pg:dbname=$domain && \
+mkdir $FREESIDE_EXPORT/export.DBI:Pg:dbname=$domain
+
diff --git a/FS/bin/freeside-addoutsourceuser b/FS/bin/freeside-addoutsourceuser
new file mode 100644
index 000000000..cbe792acc
--- /dev/null
+++ b/FS/bin/freeside-addoutsourceuser
@@ -0,0 +1,18 @@
+#!/bin/sh
+
+username=$1
+domain=$2
+password=$3
+realdomain=$4
+FREESIDE_CONF=%%%FREESIDE_CONF%%%
+
+freeside-adduser -s conf.DBI:Pg:dbname=$domain/secrets \
+ -n \
+ $username #2>/dev/null
+
+[ -e $FREESIDE_CONF/dbdef.DBI:Pg:dbname=$domain ] \
+ || ( freeside-setup -d $realdomain -u $username )
+
+freeside-adduser -g 1 $username
+
+htpasswd -b $FREESIDE_CONF/htpasswd $username $password
diff --git a/FS/bin/freeside-adduser b/FS/bin/freeside-adduser
new file mode 100644
index 000000000..237e29ef8
--- /dev/null
+++ b/FS/bin/freeside-adduser
@@ -0,0 +1,119 @@
+#!/usr/bin/perl -w
+
+use strict;
+use vars qw($opt_s $opt_g $opt_n);
+use Fcntl qw(:flock);
+use Getopt::Std;
+
+my $FREESIDE_CONF = "%%%FREESIDE_CONF%%%";
+
+getopts("s:g:n");
+my $user = shift or die &usage;
+
+if ( $opt_s ) {
+
+ #if ( -e "$FREESIDE_CONF/mapsecrets" ) {
+ # open(MAPSECRETS,"<$FREESIDE_CONF/mapsecrets")
+ # or die "can't open $FREESIDE_CONF/mapsecrets: $!";
+ # while (<MAPSECRETS>) {
+ # /^(\S+) / or die "unparsable line in mapsecrets: $_";
+ # die "user $user already exists\n" if $user eq $1;
+ # }
+ # close MAPSECRETS;
+ #}
+
+ #insert new entry before a wildcard...
+ open(MAPSECRETS,"<$FREESIDE_CONF/mapsecrets")
+ and flock(MAPSECRETS,LOCK_EX)
+ or die "can't open $FREESIDE_CONF/mapsecrets: $!";
+ open(NEW,">$FREESIDE_CONF/mapsecrets.new")
+ or die "can't open $FREESIDE_CONF/mapsecrets.new: $!";
+ while(<MAPSECRETS>) {
+ if ( /^\*\s/ ) {
+ print NEW "$user $opt_s\n";
+ }
+ print NEW $_;
+ }
+ close MAPSECRETS or die "can't close $FREESIDE_CONF/mapsecrets: $!";
+ close NEW or die "can't close $FREESIDE_CONF/mapsecrets.new: $!";
+ rename("$FREESIDE_CONF/mapsecrets.new", "$FREESIDE_CONF/mapsecrets")
+ or die "can't move mapsecrets.new into place: $!";
+
+}
+
+###
+
+exit if $opt_n;
+
+###
+
+use FS::UID qw(adminsuidsetup);
+use FS::CurrentUser;
+use FS::access_user;
+use FS::access_usergroup;
+
+$FS::CurrentUser::upgrade_hack = 1;
+#adminsuidsetup $rootuser;
+adminsuidsetup $user;
+
+my $access_user = new FS::access_user {
+ 'username' => $user,
+ '_password' => 'notyet',
+ 'first' => 'Firstname', # $opt_f ||
+ 'last' => 'Lastname', # $opt_l ||
+};
+my $au_error = $access_user->insert;
+die $au_error if $au_error;
+
+if ( $opt_g ) {
+
+ my $access_usergroup = new FS::access_usergroup {
+ 'usernum' => $access_user->usernum,
+ 'groupnum' => $opt_g,
+ };
+ my $aug_error = $access_usergroup->insert;
+ die $aug_error if $aug_error;
+
+}
+
+###
+
+sub usage {
+ die "Usage:\n\n freeside-adduser [ -n ] [ -s ] [ -g groupnum ] username [ password ]"
+}
+
+=head1 NAME
+
+freeside-adduser - Command line interface to add (freeside) users.
+
+=head1 SYNOPSIS
+
+ freeside-adduser [ -n ] [ -s ] [ -g groupnum ] username [ password ]
+
+=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.
+
+This functionality is now available in the web interface as well, under
+B<Configuration | Employees | View/Edit employees>.
+
+ -g: initial groupnum
+
+ Development/multi-DB options:
+
+ -s: alternate secrets file
+
+ -n: no ACL added, for bootstrapping
+
+=head1 NOTE
+
+No explicit htpasswd options are available in 1.7 - passwordsa are now
+maintained automatically.
+
+=head1 SEE ALSO
+
+Base Freeside documentation
+
+=cut
+
diff --git a/FS/bin/freeside-apply-credits b/FS/bin/freeside-apply-credits
new file mode 100755
index 000000000..ea6a7bdd0
--- /dev/null
+++ b/FS/bin/freeside-apply-credits
@@ -0,0 +1,21 @@
+#!/usr/bin/perl -Tw
+
+use strict;
+use vars qw( $user $cust_main @customers );
+use FS::UID qw(adminsuidsetup);
+use FS::Record qw(qsearch);
+use FS::cust_main;
+
+$user = shift or die &usage;
+&adminsuidsetup( $user );
+
+my @customers = qsearch('cust_main', {} );
+die "No customers" unless (scalar(@customers) > 0);
+
+foreach $cust_main (@customers) {
+ print "Applying credits for customer #". $cust_main->custnum;
+ $cust_main->apply_credits;
+}
+
+
+
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-daily b/FS/bin/freeside-daily
new file mode 100755
index 000000000..a06a2b185
--- /dev/null
+++ b/FS/bin/freeside-daily
@@ -0,0 +1,92 @@
+#!/usr/bin/perl -w
+
+use strict;
+use Getopt::Std;
+use FS::UID qw(adminsuidsetup);
+
+&untaint_argv; #what it sounds like (eww)
+#use vars qw($opt_d $opt_v $opt_p $opt_a $opt_s $opt_y);
+use vars qw(%opt);
+getopts("p:a:d:vsy:", \%opt);
+
+my $user = shift or die &usage;
+adminsuidsetup $user;
+
+use FS::Cron::bill qw(bill);
+bill(%opt);
+
+use FS::Cron::notify qw(notify_flat_delay);
+notify_flat_delay(%opt);
+
+use FS::Cron::vacuum qw(vacuum);
+vacuum();
+
+use FS::Cron::backup qw(backup_scp);
+backup_scp();
+
+###
+# subroutines
+###
+
+sub untaint_argv {
+ foreach $_ ( $[ .. $#ARGV ) { #untaint @ARGV
+ #$ARGV[$_] =~ /^([\w\-\/]*)$/ || die "Illegal arguement \"$ARGV[$_]\"";
+ # Date::Parse
+ $ARGV[$_] =~ /^(.*)$/ || die "Illegal arguement \"$ARGV[$_]\"";
+ $ARGV[$_]=$1;
+ }
+}
+
+sub usage {
+ die "Usage:\n\n freeside-daily [ -d 'date' ] user [ custnum custnum ... ]\n";
+}
+
+###
+# documentation
+###
+
+=head1 NAME
+
+freeside-daily - Run daily billing and invoice collection events.
+
+=head1 SYNOPSIS
+
+ freeside-daily [ -d 'date' ] [ -y days ] [ -p 'payby' ] [ -a agentnum ] [ -s ] [ -v ] user [ custnum custnum ... ]
+
+=head1 DESCRIPTION
+
+Bills customers and runs invoice collection events. Should be run from
+crontab daily.
+
+Bills customers. Searches for customers who are due for billing and calls
+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>)
+
+ -a: Only process customers with the specified agentnum
+
+ -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
+customers. Otherwise, bills all customers.
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::cust_main>, config.html from the base documentation
+
+=cut
+
diff --git a/FS/bin/freeside-deloutsource b/FS/bin/freeside-deloutsource
new file mode 100644
index 000000000..afc3a0118
--- /dev/null
+++ b/FS/bin/freeside-deloutsource
@@ -0,0 +1,14 @@
+#!/bin/sh
+
+domain=$1
+FREESIDE_CONF=%%%FREESIDE_CONF%%%
+FREESIDE_CACHE=%%%FREESIDE_CACHE%%%
+FREESIDE_EXPORT=%%%FREESIDE_EXPORT%%%
+
+dropdb $domain && \
+rm -rf $FREESIDE_CONF/conf.DBI:Pg:host=localhost\;dbname=$domain && \
+rm -rf $FREESIDE_CACHE/counters.DBI:Pg:host=localhost\;dbname=$domain && \
+rm -rf $FREESIDE_CACHE/cache.DBI:Pg:host=localhost\;dbname=$domain && \
+rm -rf $FREESIDE_EXPORT/export.DBI:Pg:host=localhost\;dbname=$domain && \
+rm $FREESIDE_CONF/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..dc4ff9cdc
--- /dev/null
+++ b/FS/bin/freeside-deloutsourceuser
@@ -0,0 +1,6 @@
+#!/bin/sh
+
+username=$1
+
+freeside-deluser -h %%%FREESIDE_CONF%%%/htpasswd $username 2>/dev/null
+
diff --git a/FS/bin/freeside-deluser b/FS/bin/freeside-deluser
new file mode 100644
index 000000000..a2a361a83
--- /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 = "%%%FREESIDE_CONF%%%";
+
+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
new file mode 100755
index 000000000..7a93f78ee
--- /dev/null
+++ b/FS/bin/freeside-email
@@ -0,0 +1,55 @@
+#!/usr/bin/perl -Tw
+
+use strict;
+use FS::UID qw(adminsuidsetup);
+use FS::Conf;
+use FS::Record qw(qsearch);
+use FS::svc_acct;
+
+&untaint_argv; #what it sounds like (eww)
+my $user = shift or die &usage;
+
+adminsuidsetup $user;
+
+my $conf = new FS::Conf;
+
+my @svc_acct = qsearch('svc_acct', {});
+my @emails = map $_->email, @svc_acct;
+
+print join("\n", @emails), "\n";
+
+# subroutines
+
+sub untaint_argv {
+ foreach $_ ( $[ .. $#ARGV ) { #untaint @ARGV
+ #$ARGV[$_] =~ /^([\w\-\/]*)$/ || die "Illegal arguement \"$ARGV[$_]\"";
+ # Date::Parse
+ $ARGV[$_] =~ /^(.*)$/ || die "Illegal arguement \"$ARGV[$_]\"";
+ $ARGV[$_]=$1;
+ }
+}
+
+sub usage {
+ die "Usage:\n\n freeside-email user\n";
+}
+
+=head1 NAME
+
+freeside-email - Prints email addresses of all users on STDOUT
+
+=head1 SYNOPSIS
+
+ freeside-email user
+
+=head1 DESCRIPTION
+
+Prints the email addresses of all customers on STDOUT, separated by newlines.
+
+user: From the mapsecrets file - see config.html from the base documentation
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+=cut
+
diff --git a/FS/bin/freeside-expiration-alerter b/FS/bin/freeside-expiration-alerter
new file mode 100755
index 000000000..e49bd62aa
--- /dev/null
+++ b/FS/bin/freeside-expiration-alerter
@@ -0,0 +1,222 @@
+#!/usr/bin/perl -Tw
+
+use strict;
+use Date::Format;
+use Time::Local;
+use Text::Template;
+use Getopt::Std;
+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;
+
+use vars qw($smtpmachine @body);
+
+#hush, perl!
+$FS::alerter::_template::first = "";
+$FS::alerter::_template::last = "";
+$FS::alerter::_template::company = "";
+$FS::alerter::_template::payby = "";
+$FS::alerter::_template::expdate = "";
+
+# Set the mail program and other variables
+my $mail_sender = "billing\@mydomain.tld"; # or invoice_from if available
+my $failure_recipient = "postmaster"; # or invoice_from if available
+my $warning_time = 30 * 24 * 60 * 60;
+my $urgent_time = 15 * 24 * 60 * 60;
+my $panic_time = 5 * 24 * 60 * 60;
+my $window_time = 24 * 60 * 60;
+
+&untaint_argv; #what it sounds like (eww)
+
+#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++;
+
+# Login to the database
+my $user = shift or die &usage;
+adminsuidsetup $user;
+
+# Get the needed configuration files
+my $conf = new FS::Conf;
+$smtpmachine = $conf->config('smtpmachine');
+$mail_sender = $conf->config('invoice_from')
+ if $conf->exists('invoice_from');
+$failure_recipient = $conf->config('invoice_from')
+ if $conf->exists('invoice_from');
+
+
+my(@customers)=qsearch('cust_main',{});
+if (scalar(@customers) == 0)
+{
+ exit 1;
+}
+
+# Prepare for sending email
+
+$ENV{MAILADDRESS} = $mail_sender;
+my $header = new Mail::Header ( [
+ "From: Account Processor",
+ "To: $failure_recipient",
+ "Sender: $mail_sender",
+ "Reply-To: $mail_sender",
+ "Subject: Unnotified Billing Arrangement Expirations",
+] );
+
+my @alerter_template = $conf->config('alerter_template')
+ or die "cannot load config file alerter_template";
+
+my $alerter = new Text::Template (TYPE => 'ARRAY', SOURCE => [ map "$_\n", @alerter_template ])
+ or die "can't create new Text::Template object: Text::Template::ERROR";
+$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 $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' || $payby eq 'DCRD') {
+ ($paymonth < 11) ? $paymonth++ : ($paymonth=0, $payyear++);
+ $expire_time = timelocal(0,0,0,$payday,$paymonth,$payyear);
+ $expire_time--;
+ }
+
+ if (($expire_time < $_date + $warning_time &&
+ $expire_time > $_date + $warning_time - $window_time) ||
+ ($expire_time < $_date + $urgent_time &&
+ $expire_time > $_date + $urgent_time - $window_time) ||
+ ($expire_time < $_date + $panic_time &&
+ $expire_time > $_date + $panic_time - $window_time)) {
+
+
+
+ my @packages = $customer->ncancelled_pkgs;
+ if (scalar(@packages) != 0) {
+ my @invoicing_list = $customer->invoicing_list;
+ if ( grep { $_ ne 'POST' } @invoicing_list ) {
+ my $header = new Mail::Header ( [
+ "From: $mail_sender",
+ "To: ". join(', ', grep { $_ ne 'POST' } @invoicing_list ),
+ "Sender: $mail_sender",
+ "Reply-To: $mail_sender",
+ "Date: ". time2str("%a, %d %b %Y %X %z", time),
+ "Subject: Billing Arrangement Expiration",
+ ] );
+ $FS::alerter::_template::first = $first;
+ $FS::alerter::_template::last = $last;
+ $FS::alerter::_template::company = $company;
+ if ($payby eq 'CARD' || $payby eq 'DCRD') {
+ $FS::alerter::_template::payby = "credit card (" .
+ substr($payinfo, 0, 2) . "xxxxxxxxxx" .
+ substr($payinfo, -4) . ")";
+ }elsif ($payby eq 'COMP') {
+ $FS::alerter::_template::payby = "complimentary account";
+ }else{
+ $FS::alerter::_template::payby = "current method";
+ }
+ $FS::alerter::_template::expdate = $expire_time;
+
+ my $message = new Mail::Internet (
+ 'Header' => $header,
+ 'Body' => [ $alerter->fill_in( PACKAGE => 'FS::alerter::_template' ) ],
+ );
+ $!=0;
+ $message->smtpsend( Host => $smtpmachine )
+ or $message->smtpsend( Host => $smtpmachine, Debug => 1 )
+ or die "Can't send expiration email: $!";
+
+ } elsif ( ! @invoicing_list || grep { $_ eq 'POST' } @invoicing_list ) {
+ push @body, sprintf(qq{%5d %-32.32s %4s %10s %12s %12s},
+ $custnum,
+ $first . " " . $last . " " . $company,
+ $payby,
+ $paydate,
+ $daytime,
+ $night);
+ }
+ }
+ }
+}
+
+# Now I need to send EMAIL
+if (scalar(@body)) {
+ my $message = new Mail::Internet (
+ 'Header' => $header,
+ 'Body' => [ (@body) ],
+ );
+ $!=0;
+ $message->smtpsend( Host => $smtpmachine )
+ or $message->smtpsend( Host => $smtpmachine, Debug => 1 )
+ or die "can't send alerter failure email to $failure_recipient".
+ " via server $smtpmachine with SMTP: $!";
+}
+
+# subroutines
+sub untaint_argv {
+ foreach $_ ( $[ .. $#ARGV ) { #untaint @ARGV
+ $ARGV[$_] =~ /^([\w\-\/]*)$/ || die "Illegal argument \"$ARGV[$_]\"";
+ $ARGV[$_]=$1;
+ }
+}
+
+sub usage {
+ die "Usage:\n\n freeside-expiration-alerter user\n";
+}
+
+=head1 NAME
+
+freeside-expiration-alerter - Emails notifications of credit card expirations.
+
+=head1 SYNOPSIS
+
+ freeside-expiration-alerter user
+
+=head1 DESCRIPTION
+
+Emails customers notice that their credit card or other billing arrangement
+is about to expire. Usually run as a cron job.
+
+user: From the mapsecrets file - see config.html from the base documentation
+
+=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>
+
+=cut
+
+
diff --git a/FS/bin/freeside-monthly b/FS/bin/freeside-monthly
new file mode 100755
index 000000000..a6c75e715
--- /dev/null
+++ b/FS/bin/freeside-monthly
@@ -0,0 +1,91 @@
+#!/usr/bin/perl -w
+
+use strict;
+use Getopt::Std;
+use FS::UID qw(adminsuidsetup);
+
+&untaint_argv; #what it sounds like (eww)
+#use vars qw($opt_d $opt_v $opt_p $opt_a $opt_s $opt_y);
+use vars qw(%opt);
+getopts("p:a:d:vsy:", \%opt);
+
+my $user = shift or die &usage;
+adminsuidsetup $user;
+
+use FS::Cron::bill qw(bill);
+bill(%opt, 'freq'=>'1m' );
+
+###
+# subroutines
+###
+
+sub untaint_argv {
+ foreach $_ ( $[ .. $#ARGV ) { #untaint @ARGV
+ #$ARGV[$_] =~ /^([\w\-\/]*)$/ || die "Illegal arguement \"$ARGV[$_]\"";
+ # Date::Parse
+ $ARGV[$_] =~ /^(.*)$/ || die "Illegal arguement \"$ARGV[$_]\"";
+ $ARGV[$_]=$1;
+ }
+}
+
+sub usage {
+ die "Usage:\n\n freeside-monthly [ -d 'date' ] user [ custnum custnum ... ]\n";
+}
+
+###
+# documentation
+###
+
+=head1 NAME
+
+freeside-monthly - Run monthly billing and invoice collection events.
+
+=head1 SYNOPSIS
+
+ freeside-monthly [ -d 'date' ] [ -y days ] [ -p 'payby' ] [ -a agentnum ] [ -s ] [ -v ] user [ custnum custnum ... ]
+
+=head1 DESCRIPTION
+
+Bills customers and runs invoice collection events, for the alternate monthly
+event chain. If you have defined monthly event checks, should be run from
+crontab monthly.
+
+Bills customers. Searches for customers who are due for billing and calls
+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>)
+
+ -a: Only process customers with the specified agentnum
+
+ -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
+customers. Otherwise, bills all customers.
+
+=head1 NOTE
+
+In most cases, you would use freeside-daily only and not freeside-monthly.
+freeside-monthly would only be used in cases where you have events that can
+only be run once each month, for example, batching invoices to a third-party
+print/mail provider.
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<freeside-daily>, L<FS::cust_main>, config.html from the base documentation
+
+=cut
+
diff --git a/FS/bin/freeside-prepaidd b/FS/bin/freeside-prepaidd
new file mode 100644
index 000000000..73f7523c4
--- /dev/null
+++ b/FS/bin/freeside-prepaidd
@@ -0,0 +1,104 @@
+#!/usr/bin/perl -w
+
+use strict;
+use FS::Daemon qw(daemonize1 drop_root logfile daemonize2 sigint sigterm);
+use FS::UID qw(adminsuidsetup);
+use FS::Record qw(qsearch qsearchs);
+use FS::cust_pkg;
+
+my $user = shift or die &usage;
+
+#daemonize1('freeside-sprepaidd', $user); #keep unique pid files w/multi installs
+daemonize1('freeside-prepaidd');
+
+drop_root();
+
+adminsuidsetup($user);
+
+logfile( "/usr/local/etc/freeside/prepaidd-log.". $FS::UID::datasrc );
+
+daemonize2();
+
+#--
+
+while (1) {
+
+ foreach my $cust_pkg (
+ qsearch( {
+ 'select' => 'cust_pkg.*, part_pkg.plan',
+ 'table' => 'cust_pkg',
+ 'addl_from' => 'LEFT JOIN part_pkg USING ( pkgpart )',
+ #'hashref' => { 'plan' => 'prepaid' },#should check part_pkg::is_prepaid
+ #'extra_sql' => "AND bill < ". time.
+ 'hashref' => {},
+ 'extra_sql' => "WHERE plan = 'prepaid' AND bill < ". time.
+ " AND bill IS NOT NULL".
+ " AND ( susp IS NULL OR susp = 0)".
+ " AND ( cancel IS NULL OR cancel = 0)"
+ } )
+ ) {
+
+ my $work_cust_pkg = $cust_pkg;
+
+ my $cust_main = $cust_pkg->cust_main;
+ if ( $cust_main->total_unapplied_payments > 0
+ or $cust_main->total_credited > 0
+ )
+ {
+ #this needs a flag to say only do the prepaid packages...
+ # and only try em if the renewal price matches.. but this will do for now
+ my $b_error = $cust_main->bill;
+ if ( $b_error ) {
+ warn "Error billing customer #". $cust_main->custnum;
+ next;
+ }
+ #$b_error = $cust_main->apply_payments_and_credits;
+ $b_error = $cust_main->apply_payments;
+ $b_error = $cust_main->apply_credits;
+
+ $work_cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $work_cust_pkg->pkgnum } );
+
+ next
+ if $cust_main->balance <= 0
+ and $work_cust_pkg->bill >= time;
+ }
+
+ my $action = $work_cust_pkg->part_pkg->option('recur_action') || 'suspend';
+
+ my $error = $work_cust_pkg->$action();
+
+ warn "Error ${action}ing package ". $work_cust_pkg->pkgnum.
+ " for custnum ". $work_cust_pkg->custnum.
+ ": $error\n"
+ if $error;
+ }
+
+ die "exiting" if sigterm() || sigint();
+ sleep 5;
+
+}
+
+#--
+
+sub usage {
+ die "Usage:\n\n freeside-prepaidd user\n";
+}
+
+=head1 NAME
+
+freeside-prepaidd - Real-time daemon for prepaid packages
+
+=head1 SYNOPSIS
+
+ freeside-prepaidd
+
+=head1 DESCRIPTION
+
+Runs continuously and suspends or cancels any prepaid customer packages which
+have passed their renewal date (next bill date).
+
+=head1 SEE ALSO
+
+=cut
+
+1;
diff --git a/FS/bin/freeside-queued b/FS/bin/freeside-queued
new file mode 100644
index 000000000..93d735d1a
--- /dev/null
+++ b/FS/bin/freeside-queued
@@ -0,0 +1,250 @@
+#!/usr/bin/perl -w
+
+use strict;
+use vars qw( $DEBUG $kids $max_kids %kids );
+use POSIX qw(:sys_wait_h);
+use IO::File;
+use FS::UID qw(adminsuidsetup forksuidsetup driver_name dbh myconnect);
+use FS::Daemon qw(daemonize1 drop_root logfile daemonize2 sigint sigterm);
+use FS::Record qw(qsearch qsearchs);
+use FS::queue;
+use FS::queue_depend;
+
+# no autoloading for non-FS classes...
+use Net::SSH 0.07;
+
+$DEBUG = 0;
+
+$max_kids = '10'; #guess it should be a config file...
+$kids = 0;
+
+my $user = shift or die &usage;
+
+warn "starting daemonization (forking)\n" if $DEBUG;
+#daemonize1('freeside-queued',$user); #to keep pid files unique w/multi installs
+daemonize1('freeside-queued');
+
+warn "dropping privledges\n" if $DEBUG;
+drop_root();
+
+
+$ENV{HOME} = (getpwuid($>))[7]; #for ssh
+
+warn "connecting to database\n" if $DEBUG;
+$@ = 'not connected';
+while ( $@ ) {
+ eval { adminsuidsetup $user; };
+ if ( $@ ) {
+ warn $@;
+ warn "sleeping for reconnect...\n";
+ sleep 5;
+ }
+}
+
+logfile( "%%%FREESIDE_LOG%%%/queuelog.". $FS::UID::datasrc );
+
+warn "completing daemonization (detaching))\n" if $DEBUG;
+daemonize2();
+
+#--
+
+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;
+ }
+ $warnkids=0;
+
+ unless ( dbh && dbh->ping ) {
+ warn "WARNING: connection to database lost, reconnecting...\n";
+
+ eval { $FS::UID::dbh = myconnect; };
+
+ unless ( !$@ && dbh && dbh->ping ) {
+ warn "WARNING: still no connection to database, sleeping for retry...\n";
+ sleep 10;
+ next;
+ } else {
+ warn "WARNING: reconnected to database\n";
+ }
+ }
+
+ #my($job, $ljob);
+ #{
+ # my $oldAutoCommit = $FS::UID::AutoCommit;
+ # local $FS::UID::AutoCommit = 0;
+ $FS::UID::AutoCommit = 0;
+
+ #assuming mysql 4.1 w/subqueries now
+ #my $nodepend = driver_name eq 'mysql'
+ # ? ''
+ # : 'AND 0 = ( SELECT COUNT(*) FROM queue_depend'.
+ # ' WHERE queue_depend.jobnum = queue.jobnum ) ';
+ my $nodepend = 'AND 0 = ( SELECT COUNT(*) FROM queue_depend'.
+ ' WHERE queue_depend.jobnum = queue.jobnum ) ';
+
+ my $job = qsearchs(
+ 'queue',
+ { 'status' => 'new' },
+ '',
+ driver_name eq 'mysql'
+ ? "$nodepend ORDER BY jobnum LIMIT 1 FOR UPDATE"
+ : "$nodepend ORDER BY jobnum FOR UPDATE LIMIT 1"
+ ) or do {
+ # if $oldAutoCommit {
+ dbh->commit or do {
+ warn "WARNING: database error, closing connection: ". dbh->errstr;
+ undef $FS::UID::dbh;
+ next;
+ };
+ # }
+ sleep 5; #connecting to db is expensive
+ next;
+ };
+
+ #assuming mysql 4.1 w/subqueries now
+ #if ( driver_name eq 'mysql'
+ # && qsearch('queue_depend', { 'jobnum' => $job->jobnum } ) ) {
+ # dbh->commit or die dbh->errstr; #if $oldAutoCommit;
+ # sleep 5; #would be better if mysql could do everything in query above
+ # next;
+ #}
+
+ my %hash = $job->hash;
+ $hash{'status'} = 'locked';
+ my $ljob = new FS::queue ( \%hash );
+ my $error = $ljob->replace($job);
+ if ( $error ) {
+ warn "WARNING: database error locking job, closing connection: ".
+ dbh->errstr;
+ undef $FS::UID::dbh;
+ next;
+ }
+
+ # if $oldAutoCommit {
+ dbh->commit or do {
+ warn "WARNING: database error, closing connection: ". dbh->errstr;
+ undef $FS::UID::dbh;
+ next;
+ };
+ # }
+
+ $FS::UID::AutoCommit = 1;
+ #}
+
+ my @args = $ljob->args;
+ splice @args, 0, 1, $ljob if $args[0] eq '_JOB';
+
+ defined( my $pid = fork ) or do {
+ warn "WARNING: can't fork: $!\n";
+ my %hash = $job->hash;
+ $hash{'status'} = 'failed';
+ $hash{'statustext'} = "[freeside-queued] can't fork: $!";
+ my $ljob = new FS::queue ( \%hash );
+ my $error = $ljob->replace($job);
+ die $error if $error;
+ next; #don't increment the kid counter
+ };
+
+ if ( $pid ) {
+ $kids++;
+ $kids{$pid} = 1;
+ } else { #kid time
+
+ #get new db handle
+ $FS::UID::dbh->{InactiveDestroy} = 1;
+
+ forksuidsetup($user);
+
+ #auto-use classes...
+ #if ( $ljob->job =~ /(FS::part_export::\w+)::/ ) {
+ if ( $ljob->job =~ /(FS::part_export::\w+)::/
+ || $ljob->job =~ /(FS::\w+)::/
+ )
+ {
+ my $class = $1;
+ eval "use $class;";
+ if ( $@ ) {
+ warn "job use $class failed";
+ my %hash = $ljob->hash;
+ $hash{'status'} = 'failed';
+ $hash{'statustext'} = $@;
+ my $fjob = new FS::queue( \%hash );
+ my $error = $fjob->replace($ljob);
+ die $error if $error;
+ exit; #end-of-kid
+ };
+ }
+
+ my $eval = "&". $ljob->job. '(@args);';
+ warn 'running "&'. $ljob->job. '('. join(', ', @args). ")\n" if $DEBUG;
+ eval $eval; #throw away return value? suppose so
+ if ( $@ ) {
+ warn "job $eval failed";
+ my %hash = $ljob->hash;
+ $hash{'status'} = 'failed';
+ $hash{'statustext'} = $@;
+ my $fjob = new FS::queue( \%hash );
+ my $error = $fjob->replace($ljob);
+ die $error if $error;
+ } else {
+ $ljob->delete;
+ }
+
+ exit;
+ #end-of-kid
+ }
+
+} continue {
+ if ( sigterm() ) {
+ warn "received TERM signal; exiting\n";
+ exit;
+ }
+ if ( sigint() ) {
+ warn "received INT signal; exiting\n";
+ exit;
+ }
+}
+
+sub usage {
+ die "Usage:\n\n freeside-queued user\n";
+}
+
+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
+
+=head1 SYNOPSIS
+
+ freeside-queued user
+
+=head1 DESCRIPTION
+
+Job queue daemon. Should be running at all times.
+
+user: from the mapsecrets file - see config.html from the base documentation
+
+=head1 VERSION
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+=cut
+
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-reexport b/FS/bin/freeside-reexport
new file mode 100644
index 000000000..54af9dd80
--- /dev/null
+++ b/FS/bin/freeside-reexport
@@ -0,0 +1,71 @@
+#!/usr/bin/perl -w
+
+use strict;
+use vars qw($opt_s $opt_u $opt_p);
+use Getopt::Std;
+use FS::UID qw(adminsuidsetup);
+use FS::Record qw(qsearch qsearchs);
+use FS::part_export;
+use FS::svc_acct;
+use FS::cust_svc;
+
+my $user = shift or die &usage;
+adminsuidsetup $user;
+
+my $export_x = shift or die &usage;
+my @part_export;
+if ( $export_x =~ /^(\d+)$/ ) {
+ @part_export = qsearchs('part_export', { exportnum=>$1 } )
+ or die "exportnum $export_x not found\n";
+} else {
+ @part_export = qsearch('part_export', { exporttype=>$export_x } )
+ or die "no exports of type $export_x found\n";
+}
+
+getopts('s:u:p:');
+
+my @svc_x = ();
+if ( $opt_s ) {
+ my $cust_svc = qsearchs('cust_svc', { svcnum=>$opt_s } )
+ or die "svcnum $opt_s not found\n";
+ push @svc_x, $cust_svc->svc_x;
+} elsif ( $opt_u ) {
+ my $svc_x = qsearchs('svc_acct', { username=>$opt_u } )
+ or die "username $opt_u not found\n";
+ push @svc_x, $svc_x;
+} elsif ( $opt_p ) {
+ push @svc_x, map { $_->svc_x } qsearch('cust_svc', { svcpart=>$opt_p } );
+ die "no services with svcpart $opt_p found\n" unless @svc_x;
+}
+
+foreach my $part_export ( @part_export ) {
+ foreach my $svc_x ( @svc_x ) {
+ my $error = $part_export->export_insert($svc_x);
+ die $error if $error;
+ }
+}
+
+
+sub usage {
+ die "Usage:\n\n freeside-reexport user exportnum|exporttype [ -s svcnum | -u username | -p svcpart ]\n";
+}
+
+=head1 NAME
+
+freeside-reexport - Command line tool to re-trigger export jobs for existing services
+
+=head1 SYNOPSIS
+
+ freeside-reexport user exportnum|exporttype [ -s svcnum | -u username | -p svcpart ]
+
+=head1 DESCRIPTION
+
+ Re-queues the export job for the specified exportnum or exporttype(s) and
+ specified service (selected by svcnum or username).
+
+=head1 SEE ALSO
+
+L<freeside-sqlradius-reset>, L<FS::part_export>
+
+=cut
+
diff --git a/FS/bin/freeside-reset-fixed b/FS/bin/freeside-reset-fixed
new file mode 100755
index 000000000..5829d441b
--- /dev/null
+++ b/FS/bin/freeside-reset-fixed
@@ -0,0 +1,69 @@
+#!/usr/bin/perl -w
+
+use strict;
+use vars qw($opt_p $opt_s $opt_r);
+use Getopt::Std;
+use FS::UID qw(adminsuidsetup);
+use FS::Record qw(qsearch qsearchs);
+use FS::cust_svc;
+use FS::svc_Common;
+
+getopts('p:s:r');
+
+my $user = shift or die &usage;
+adminsuidsetup $user;
+
+die &usage
+ if ($opt_p && $opt_s);
+
+$FS::Record::nowarn_identical = 1;
+$FS::svc_Common::noexport_hack = 1
+ unless $opt_r;
+
+my @svc_x = ();
+if ( $opt_s ) {
+ $opt_s =~ /^(\d+)$/ or die "invalid svcnum";
+ my $cust_svc = qsearchs('cust_svc', { svcnum => $1 } )
+ or die "svcnum $opt_s not found\n";
+ push @svc_x, $cust_svc->svc_x;
+} elsif ( $opt_p ) {
+ $opt_p =~ /^(\d+)$/ or die "invalid svcpart";
+ push @svc_x, map { $_->svc_x } qsearch('cust_svc', { svcpart => $1 } );
+ die "no services with svcpart $opt_p found\n" unless @svc_x;
+} else {
+ push @svc_x, map { $_->svc_x } qsearch('cust_svc', {} );
+ die "no services found\n" unless @svc_x;
+}
+
+foreach my $svc_x ( @svc_x ) {
+ my $result = $svc_x->setfixed;
+ die $result unless ref($result);
+ my $error = $svc_x->replace
+ if $svc_x->modified;
+ die $error if $error;
+}
+
+
+sub usage {
+ die "Usage:\n\n freeside-reset-fixed user [ -s svcnum | -p svcpart ] [ -r ]\n";
+}
+
+=head1 NAME
+
+freeside-reset-fixed - Command line tool to set the fixed columns for existing services
+
+=head1 SYNOPSIS
+
+ freeside-reset-fixed user [ -s svcnum | -p svcpart ] [ -r ]
+
+=head1 DESCRIPTION
+
+ Resets the fixed columns for the specified service part or service number.
+ Re-exports the service if -r is specified.
+
+=head1 SEE ALSO
+
+L<freeside-reexport>, L<FS::part_svc>
+
+=cut
+
diff --git a/FS/bin/freeside-selfservice-server b/FS/bin/freeside-selfservice-server
new file mode 100644
index 000000000..187bc1469
--- /dev/null
+++ b/FS/bin/freeside-selfservice-server
@@ -0,0 +1,229 @@
+#!/usr/bin/perl -w
+
+use strict;
+use vars qw( $FREESIDE_LOG $FREESIDE_LOCK );
+use vars qw( $Debug %kids $kids $max_kids $ssh_pid %old_ssh_pid $keepalives );
+use subs qw( lock_write unlock_write myshutdown usage );
+use Fcntl qw(:flock);
+use POSIX qw(:sys_wait_h);
+use IO::Handle;
+use IO::Select;
+use IO::File;
+use Storable 2.09 qw(nstore_fd fd_retrieve);
+use Net::SSH qw(sshopen2);
+use FS::Daemon qw(daemonize1 drop_root logfile daemonize2 sigint sigterm);
+use FS::UID qw(adminsuidsetup forksuidsetup);
+use FS::ClientAPI;
+
+use FS::Conf;
+use FS::cust_bill;
+use FS::cust_pkg;
+
+$FREESIDE_LOG = "%%%FREESIDE_LOG%%%";
+$FREESIDE_LOCK = "%%%FREESIDE_LOCK%%%";
+
+$Debug = 1; # 2 will turn on more logging
+ # 3 will log packet contents, including passwords
+
+$max_kids = '10'; #?
+$keepalives = 0; #let clientd turn it on, so we don't barf on old ones
+$kids = 0;
+
+my $user = shift or die &usage;
+my $machine = shift or die &usage;
+my $tag = scalar(@ARGV) ? shift : '';
+
+my $lock_file = "$FREESIDE_LOCK/selfservice.$machine.writelock";
+
+# to keep pid files unique w/multi machines (and installs!)
+# $FS::UID::datasrc not posible
+daemonize1("freeside-selfservice-server","$user.$machine");
+
+#false laziness w/Daemon::drop_root
+my $freeside_gid = scalar(getgrnam('freeside'))
+ or die "can't find freeside group\n";
+
+open(LOCKFILE,">$lock_file") or die "can't open $lock_file: $!";
+chown $FS::UID::freeside_uid, $freeside_gid, $lock_file;
+
+drop_root();
+
+$ENV{HOME} = (getpwuid($>))[7]; #for ssh
+
+adminsuidsetup $user;
+
+#logfile("/usr/local/etc/freeside/selfservice.". $FS::UID::datasrc); #MACHINE
+logfile("$FREESIDE_LOG/selfservice.$machine.log");
+
+daemonize2();
+
+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 $keepalive_count = 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 ) {
+ myshutdown() if sigint() || sigterm();
+ if ( $keepalives && $keepalive_count++ > 10 ) {
+ $keepalive_count = 0;
+ lock_write;
+ nstore_fd( { _token => '_keepalive' }, $writer );
+ unlock_write;
+ }
+ 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;
+ $old_ssh_pid{$ssh_pid} = 1;
+ $ssh_pid = 0;
+ }
+ last;
+ }
+ warn "packet received\n".
+ join('', map { " $_=>$packet->{$_}\n" } keys %$packet )
+ if $Debug > 2;
+
+ if ( $packet->{_packet} eq '_enable_keepalive' ) {
+ warn "enabling keep alives\n" if $Debug;
+ $keepalives=1;
+ next;
+ }
+
+ #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);
+
+ #get db handle
+ #adminsuidsetup($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
+
+ open(LOCKFILE,">$lock_file") or die "can't open $lock_file: $!";
+ lock_write;
+ warn "sending response\n" if $Debug;
+ 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
+ }
+
+ }
+
+ myshutdown if sigint() || sigterm();
+ 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};
+ }
+ }
+
+ foreach my $pid ( keys %old_ssh_pid ) {
+ waitpid($pid, WNOHANG) and delete $old_ssh_pid{$pid};
+ }
+ #warn "done reaping\n";
+}
+
+sub myshutdown {
+ &reap_kids;
+ my $wait = 12; #wait up to 1 minute
+ while ( $kids > 0 && $wait-- ) {
+ warn "waiting for $kids children to terminate";
+ sleep 5;
+ &reap_kids;
+ }
+ warn "abandoning $kids children" if $kids;
+ kill 'TERM', $ssh_pid if $ssh_pid;
+ die "exiting";
+}
+
+sub lock_write {
+ warn "locking $lock_file mutex for write to write stream\n" if $Debug > 1;
+
+ #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 {
+ warn "unlocking $lock_file mutex\n" if $Debug > 1;
+
+ #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-setinvoice b/FS/bin/freeside-setinvoice
new file mode 100644
index 000000000..708e2fa30
--- /dev/null
+++ b/FS/bin/freeside-setinvoice
@@ -0,0 +1,42 @@
+#!/usr/bin/perl
+
+use strict;
+use FS::UID qw(adminsuidsetup);
+use FS::Conf;
+use FS::Record qw(qsearch qsearchs);
+use FS::cust_main;
+use FS::svc_acct;
+
+&untaint_argv; #what it sounds like (eww)
+my $user = shift or die &usage;
+
+adminsuidsetup $user;
+
+foreach my $cust_main (
+ grep { ! scalar($_->invoicing_list) }
+ qsearch( 'cust_main', {} )
+) {
+ my @dest;
+ my @cust_pkg = $cust_main->ncancelled_pkgs;
+ foreach my $cust_pkg ( @cust_pkg ) {
+ foreach my $cust_svc ( $cust_pkg->cust_svc ) {
+ my $svc_acct = qsearchs( 'svc_acct', { 'svcnum' => $cust_svc->svcnum } );
+ push @dest, $svc_acct->svcnum if $svc_acct;
+ }
+ }
+ push @dest, 'POST' unless @dest;
+ $cust_main->invoicing_list(\@dest);
+}
+
+sub untaint_argv {
+ foreach $_ ( $[ .. $#ARGV ) { #untaint @ARGV
+ $ARGV[$_] =~ /^(.*)$/ || die "Illegal arguement \"$ARGV[$_]\"";
+ $ARGV[$_]=$1;
+ }
+}
+
+sub usage {
+ die "Usage:\n\n freeside-setinvoice user\n";
+}
+
+
diff --git a/FS/bin/freeside-setup b/FS/bin/freeside-setup
new file mode 100755
index 000000000..ddc210f50
--- /dev/null
+++ b/FS/bin/freeside-setup
@@ -0,0 +1,130 @@
+#!/usr/bin/perl -Tw
+
+#to delay loading dbdef until we're ready
+BEGIN { $FS::Schema::setup_hack = 1; }
+
+use strict;
+use vars qw($opt_u $opt_d $opt_v);
+use Getopt::Std;
+use FS::UID qw(adminsuidsetup datasrc checkeuid getsecrets);
+use FS::CurrentUser;
+use FS::Schema qw( dbdef_dist reload_dbdef );
+use FS::Record;
+#use FS::raddb;
+use FS::Setup qw(create_initial_data);
+
+die "Not running uid freeside!" unless checkeuid();
+
+#my %attrib2db =
+# map { lc($FS::raddb::attrib{$_}) => $_ } keys %FS::raddb::attrib;
+
+getopts("u:vd:");
+#my $user = shift or die &usage;
+
+getsecrets($opt_u); #$user);
+
+#needs to match FS::Record
+my($dbdef_file) = "%%%FREESIDE_CONF%%%/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;
+
+###
+# create a dbdef object from the old data structure
+###
+
+my $dbdef = dbdef_dist;
+
+#important
+$dbdef->save($dbdef_file);
+&FS::Schema::reload_dbdef($dbdef_file);
+
+###
+# create 'em
+###
+
+$FS::CurrentUser::upgrade_hack = 1;
+my $dbh = adminsuidsetup $opt_u; #$user;
+
+#create tables
+$|=1;
+
+foreach my $statement ( $dbdef->sql($dbh) ) {
+ warn $statement if $statement =~ /TABLE cdr/;
+ $dbh->do( $statement )
+ or die "CREATE error: ". $dbh->errstr. "\ndoing statement: $statement";
+}
+
+#now go back and reverse engineer the db
+#so we pick up the correct column DEFAULTs for #oidless inserts
+dbdef_create($dbh, $dbdef_file);
+delete $FS::Schema::dbdef_cache{$dbdef_file}; #force an actual reload
+reload_dbdef($dbdef_file);
+
+create_initial_data('domain' => $opt_d);
+
+warn "Freeside database initialized - commiting transaction\n" if $opt_v;
+
+$dbh->commit or die $dbh->errstr;
+$dbh->disconnect or die $dbh->errstr;
+
+warn "Database initialization committed successfully\n" if $opt_v;
+
+sub dbdef_create { # reverse engineer the schema from the DB and save to file
+ my( $dbh, $file ) = @_;
+ my $dbdef = new_native DBIx::DBSchema $dbh;
+ $dbdef->save($file);
+}
+
+sub usage {
+ die "Usage:\n freeside-setup -d domain.name [ -v ]\n"
+ # [ -u user ] for devel/multi-db installs
+}
+
+1;
+
+
diff --git a/FS/bin/freeside-sqlradius-radacctd b/FS/bin/freeside-sqlradius-radacctd
new file mode 100644
index 000000000..83fd4bfd1
--- /dev/null
+++ b/FS/bin/freeside-sqlradius-radacctd
@@ -0,0 +1,150 @@
+#!/usr/bin/perl -w
+
+use strict;
+use vars qw( @part_export );
+use subs qw(myshutdown);
+use POSIX qw(:sys_wait_h);
+#use IO::File;
+use FS::Daemon qw(daemonize1 drop_root logfile daemonize2 sigint sigterm);
+use FS::UID qw(adminsuidsetup); #forksuidsetup driver_name dbh myconnect);
+use FS::Record qw(qsearch); # qsearchs);
+use FS::part_export;
+#use FS::svc_acct;
+#use FS::cust_svc;
+
+my $user = shift or die &usage;
+
+#daemonize1('freeside-sqlradius-radacctd', $user); #keep unique pid files w/multi installs
+daemonize1('freeside-sqlradius-radacctd');
+
+drop_root();
+
+#$ENV{HOME} = (getpwuid($>))[7]; #for ssh
+
+adminsuidsetup $user;
+
+logfile( "%%%FREESIDE_LOG%%%/sqlradius-radacctd-log.". $FS::UID::datasrc );
+
+daemonize2();
+
+#--
+
+#don't just look for ->can('usage_sessions'), we're sqlradius-specific
+# (radiator is supposed to be setup with a radacct table)
+
+@part_export =
+ qsearch('part_export', { 'exporttype' => 'sqlradius' } );
+push @part_export,
+ qsearch('part_export', { 'exporttype' => 'sqlradius_withdomain' } );
+push @part_export,
+ qsearch('part_export', { 'exporttype' => 'radiator' } );
+
+@part_export = grep { ! $_->option('ignore_accounting') } @part_export;
+
+die "no sqlradius, sqlradius_withdomain or radiator exports without".
+ " ignore_accounting"
+ unless @part_export;
+
+while (1) {
+
+ #fork off one kid per export (machine)
+ # _>{'_radacct_kid'} is an evil kludge
+ foreach my $part_export ( grep ! $_->{'_radacct_kid'}, @part_export ) {
+
+ defined( my $pid = fork ) or do {
+ warn "WARNING: can't fork to spawn child for ". $part_export->machine;
+ next;
+ };
+
+ if ( $pid ) {
+ $part_export->{'_radacct_kid'} = $pid;
+ warn "child $pid spawned for ". $part_export->machine;
+ } else { #kid time
+
+ adminsuidsetup($user); #get our own db handle
+
+ until ( sigint || sigterm ) {
+ $part_export->update_svc_acct();
+ sleep 1;
+ }
+
+ warn "child for ". $part_export->machine. " done";
+ exit;
+
+ } #eo kid
+
+ }
+
+ #reap up any kids that died...
+ &reap_kids;
+
+ myshutdown() if sigterm() || sigint();
+
+ sleep 5;
+}
+
+#--
+
+sub myshutdown {
+ &reap_kids;
+
+ #kill all the kids
+ kill 'TERM', $_ foreach grep $_, map $_->{'_radacct_kid'}, @part_export;
+
+ my $wait = 12; #wait up to 1 minute
+ while ( ( grep $_->{'_radacct_kid'}, @part_export ) && $wait-- ) {
+ warn "waiting for children to terminate";
+ sleep 5;
+ &reap_kids;
+ }
+ warn "abandoning children" if grep $_->{'_radacct_kid'}, @part_export;
+ die "exiting";
+}
+
+sub reap_kids {
+ #warn "reaping kids\n";
+ foreach my $part_export ( grep $_->{'_radacct_kid'}, @part_export ) {
+ my $pid = $part_export->{'_radacct_kid'};
+ my $kid = waitpid($pid, WNOHANG);
+ if ( $kid > 0 ) {
+ $part_export->{'_radacct_kid'} = '';
+ }
+ }
+ #warn "done reaping\n";
+}
+
+sub usage {
+ die "Usage:\n\n freeside-sqlradius-radacctd user\n";
+}
+
+=head1 NAME
+
+freeside-sqlradius-radacctd - Real-time radacct import daemon
+
+=head1 SYNOPSIS
+
+ freeside-sqlradius-radacctd username
+
+=head1 DESCRIPTION
+
+Imports records from an the SQL radacct tables of all sqlradius,
+sqlradius_withdomain and radiator exports (except those with the
+ignore_accounting flag) and updates the svc_acct.seconds for each account.
+Runs as a daemon and updates the database in real-time.
+
+B<username> is a username added by freeside-adduser.
+
+=head1 RADIUS DATABASE CHANGES
+
+ALTER TABLE radacct ADD COLUMN FreesideStatus varchar(32) NULL;
+
+If you want to ignore the existing accountg records, also do:
+
+UPDATE radacct SET FreesideStatus = 'done' WHERE FreesideStatus IS NULL;
+
+=head1 SEE ALSO
+
+=cut
+
+1;
+
diff --git a/FS/bin/freeside-sqlradius-reset b/FS/bin/freeside-sqlradius-reset
new file mode 100755
index 000000000..2ac5012d4
--- /dev/null
+++ b/FS/bin/freeside-sqlradius-reset
@@ -0,0 +1,88 @@
+#!/usr/bin/perl -w
+
+use strict;
+use FS::UID qw(adminsuidsetup);
+use FS::Record qw(qsearch qsearchs);
+use FS::part_export;
+use FS::svc_acct;
+use FS::cust_svc;
+
+my $user = shift or die &usage;
+adminsuidsetup $user;
+
+#my $machine = shift or die &usage;
+
+my @exports = ();
+if ( @ARGV ) {
+ foreach my $exportnum ( @ARGV ) {
+ foreach my $exporttype (qw( sqlradius sqlradius_withdomain )) {
+ push @exports, qsearch('part_export', { exportnum => $exportnum,
+ exporttype => $exporttype, } );
+ }
+ }
+ } else {
+ @exports = qsearch('part_export', { exporttype=>'sqlradius' } );
+ push @exports, qsearch('part_export', { exporttype=>'sqlradius_withdomain' } );
+}
+
+foreach my $export ( @exports ) {
+ my $icradius_dbh = DBI->connect(
+ map { $export->option($_) } qw( datasrc username password )
+ ) or die $DBI::errstr;
+ for my $table (qw( radcheck radreply usergroup )) {
+ my $sth = $icradius_dbh->prepare("DELETE FROM $table");
+ $sth->execute or die "Can't reset $table table: ". $sth->errstr;
+ }
+ $icradius_dbh->disconnect;
+}
+
+foreach my $export ( @exports ) {
+
+ #my @svcparts = map { $_->svcpart } $export->export_svc;
+
+ my @svc_acct =
+ map { qsearchs('svc_acct', { 'svcnum' => $_->svcnum } ) }
+ map { qsearch('cust_svc', { 'svcpart' => $_->svcpart } ) }
+ grep { qsearch('cust_svc', { 'svcpart' => $_->svcpart } ) }
+ $export->export_svc;
+
+ foreach my $svc_acct ( @svc_acct ) {
+
+ $svc_acct->check; #set any fixed usergroup so it'll export even if all
+ #svc_acct records don't have the group yet
+
+ #false laziness with FS::svc_acct::insert (like it matters)
+ my $error = $export->export_insert($svc_acct);
+ die $error if $error;
+
+ }
+}
+
+sub usage {
+ die "Usage:\n\n freeside-sqlradius-reset user [ exportnum, ... ]\n";
+}
+
+=head1 NAME
+
+freeside-sqlradius-reset - Command line interface to reset and recreate RADIUS SQL tables
+
+=head1 SYNOPSIS
+
+ freeside-sqlradius-reset username [ EXPORTNUM, ... ]
+
+=head1 DESCRIPTION
+
+Deletes the radcheck, radreply and usergroup tables and repopulates them from
+the Freeside database, for the specified exports, or, if no exports are
+specified, for all sqlradius and sqlradius_withdomain exports.
+
+B<username> is a username added by freeside-adduser.
+
+=head1 SEE ALSO
+
+L<freeside-reexport>, L<FS::part_export>, L<FS::part_export::sqlradius>
+
+=cut
+
+
+
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-upgrade b/FS/bin/freeside-upgrade
new file mode 100755
index 000000000..3a4e4f8e3
--- /dev/null
+++ b/FS/bin/freeside-upgrade
@@ -0,0 +1,85 @@
+#!/usr/bin/perl -w
+
+use strict;
+use vars qw($opt_d $opt_q $opt_v);
+use vars qw($DEBUG $DRY_RUN);
+use Getopt::Std;
+use DBIx::DBSchema 0.31;
+use FS::UID qw(adminsuidsetup checkeuid datasrc ); #getsecrets);
+use FS::CurrentUser;
+use FS::Schema qw( dbdef dbdef_dist reload_dbdef );
+
+die "Not running uid freeside!" unless checkeuid();
+
+getopts("dq");
+
+$DEBUG = !$opt_q;
+#$DEBUG = $opt_v;
+
+$DRY_RUN = $opt_d;
+
+my $user = shift or die &usage;
+$FS::CurrentUser::upgrade_hack = 1;
+my $dbh = adminsuidsetup($user);
+
+#needs to match FS::Schema...
+my $dbdef_file = "%%%FREESIDE_CONF%%%/dbdef.". datasrc;
+
+dbdef_create($dbh, $dbdef_file);
+
+delete $FS::Schema::dbdef_cache{$dbdef_file}; #force an actual reload
+reload_dbdef($dbdef_file);
+
+$DBIx::DBSchema::DEBUG = $DEBUG;
+$DBIx::DBSchema::Table::DEBUG = $DEBUG;
+
+if ( $DRY_RUN ) {
+ print join(";\n", dbdef->sql_update_schema( dbdef_dist, $dbh ) ). ";\n";
+ exit;
+} else {
+ dbdef->update_schema( dbdef_dist, $dbh );
+}
+
+$dbh->commit or die $dbh->errstr;
+
+dbdef_create($dbh, $dbdef_file);
+
+$dbh->disconnect or die $dbh->errstr;
+
+###
+
+sub dbdef_create { # reverse engineer the schema from the DB and save to file
+ my( $dbh, $file ) = @_;
+ my $dbdef = new_native DBIx::DBSchema $dbh;
+ $dbdef->save($file);
+}
+
+sub usage {
+ die "Usage:\n freeside-upgrade [ -d ] [ -q | -v ] user\n";
+}
+
+=head1 NAME
+
+freeside-upgrade - Upgrades database schema for new freeside verisons.
+
+=head1 SYNOPSIS
+
+ freeside-adduser [ -d ] [ -q | -v ]
+
+=head1 DESCRIPTION
+
+Reads your existing database schema and updates it to match the current schema,
+adding any columns or tables necessary.
+
+ [ -d ]: Dry run; output SQL statements (to STDOUT) only, but do not execute
+ them.
+
+ [ -q ]: Run quietly. This may become the default at some point.
+
+ [ -v ]: Run verbosely, sending debugging information to STDERR. This is the
+ current default.
+
+=head1 SEE ALSO
+
+=cut
+