diff options
Diffstat (limited to 'FS/FS/Cron')
-rw-r--r-- | FS/FS/Cron/alert_expiration.pm | 177 | ||||
-rw-r--r-- | FS/FS/Cron/bill.pm | 234 | ||||
-rw-r--r-- | FS/FS/Cron/check.pm | 200 | ||||
-rw-r--r-- | FS/FS/Cron/notify.pm | 4 | ||||
-rw-r--r-- | FS/FS/Cron/upload.pm | 176 |
5 files changed, 714 insertions, 77 deletions
diff --git a/FS/FS/Cron/alert_expiration.pm b/FS/FS/Cron/alert_expiration.pm new file mode 100644 index 0000000..a9b9da9 --- /dev/null +++ b/FS/FS/Cron/alert_expiration.pm @@ -0,0 +1,177 @@ +package FS::Cron::alert_expiration; + +use vars qw( @ISA @EXPORT_OK); +use Exporter; +use FS::Record qw(qsearch); +use FS::Conf; +use FS::cust_main; +use FS::Misc; +use Time::Local; +use Date::Parse qw(str2time); + + +@ISA = qw( Exporter ); +@EXPORT_OK = qw( alert_expiration ); + +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; + +sub alert_expiration { + my $conf = new FS::Conf; + my $smtpmachine = $conf->config('smtpmachine'); + + my %opt = @_; + my ($_date) = $opt{'d'} ? str2time($opt{'d'}) : $^T; + $_date += $opt{'y'} * 86400 if $opt{'y'}; + my ($sec, $min, $hour, $mday, $mon, $year) = (localtime($_date)) [0..5]; + $mon++; + + my $debug = 0; + $debug = 1 if $opt{'v'}; + $debug = $opt{'l'} if $opt{'l'}; + + $FS::cust_main::DEBUG = $debug; + + # Get a list of customers. + + my %limit; + $limit{'agentnum'} = $opt{'a'} if $opt{'a'}; + $limit{'payby'} = $opt{'p'} if $opt{'p'}; + + my @customers; + + if(my @custnums = @ARGV) { + # We're given an explicit list of custnums, so select those. Then check against + # -a and -p to avoid doing anything unexpected. + foreach (@custnums) { + my $customer = FS::cust_main->by_key($_); + if($customer and (!$opt{'a'} or $customer->agentnum == $opt{'a'}) + and (!$opt{'p'} or $customer->payby eq $opt{'p'}) ) { + push @customers, $customer; + } + } + } + else { # no @ARGV + @customers = qsearch('cust_main', \%limit); + } + return if(!@customers); + foreach my $customer (@customers) { + my $paydate = $customer->paydate; + next if $paydate =~ /^\s*$/; # skip empty expiration dates + + my $custnum = $customer->custnum; + my $first = $customer->first; + my $last = $customer->last; + my $company = $customer->company; + my $payby = $customer->payby; + my $payinfo = $customer->payinfo; + my $daytime = $customer->daytime; + my $night = $customer->night; + + my ($paymonth, $payyear) = $customer->paydate_monthyear; + $paymonth--; # localtime() convention + $payday = 1; # This is enforced by FS::cust_main::check. + my $expire_time; + if($payby eq 'CARD' || $payby eq 'DCRD') { + # Credit cards expire at the end of the month/year. + if($paymonth == 11) { + $payyear++; + $paymonth = 0; + } else { + $paymonth++; + } + $expire_time = timelocal(0,0,0,$payday,$paymonth,$payyear) - 1; + } + else { + $expire_time = timelocal(0,0,0,$payday,$paymonth,$payyear); + } + + if (grep { $expire_time < $_date + $_ && + $expire_time > $_date + $_ - $window_time } + ($warning_time, $urgent_time, $panic_time) ) { + my $agentnum = $customer->agentnum; + $mail_sender = $conf->config('invoice_from', $agentnum); + $failure_recipient = $conf->config('invoice_from', $agentnum) + || 'postmaster'; + + my @alerter_template = $conf->config('alerter_template', $agentnum) + 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 Text::Template object: $Text::Template::ERROR"; + + $alerter->compile() + or die "can't compile template: $Text::Template::ERROR"; + + my @packages = $customer->ncancelled_pkgs; + if(@packages) { + my @invoicing_list = $customer->invoicing_list; + my @to_addrs = grep { $_ ne 'POST' } @invoicing_list; + if(@to_addrs) { + # Set up template fields. + my %fill_in; + $fill_in{$_} = $customer->getfield($_) + foreach(qw(first last company)); + $fill_in{'expdate'} = $expire_time; + $fill_in{'company_name'} = $conf->config('company_name', $agentnum); + $fill_in{'company_address'} = + join("\n",$conf->config('company_address',$agentnum))."\n"; + if($payby eq 'CARD' || $payby eq 'DCRD') { + $fill_in{'payby'} = "credit card (". + substr($customer->payinfo, 0, 2) . "xxxxxxxxxx" . + substr($payinfo, -4) . ")"; + } + elsif($payby eq 'COMP') { + $fill_in{'payby'} = 'complimentary account'; + } + else { + $fill_in{'payby'} = 'current method'; + } + # Send it already! + my $error = FS::Misc::send_email ( + from => $mail_sender, + to => [ @to_addrs ], + subject => 'Billing Arrangement Expiration', + body => [ $alerter->fill_in( HASH => \%fill_in ) ], + ); + die "can't send expiration alert: $error" + if $error; + } + else { # if(@to_addrs) + push @{$agent_failure_body{$customer->agentnum}}, + sprintf(qq{%5d %-32.32s %4s %10s %12s %12s}, + $custnum, + $first . " " . $last . " " . $company, + $payby, + $paydate, + $daytime, + $night ); + } + } # if(@packages) + } # if(expired) + } # foreach(@customers) + + # Failure notification + foreach my $agentnum (keys %agent_failure_body) { + $mail_sender = $conf->config('invoice_from', $agentnum) + if($conf->exists('invoice_from', $agentnum)); + $failure_recipient = $conf->config('invoice_from', $agentnum) + if($conf->exists('invoice_from', $agentnum)); + my $error = FS::Misc::send_email ( + from => $mail_sender, + to => $failure_recipient, + subject => 'Unnotified Billing Arrangement Expirations', + body => [ @{$agent_failure_body{$agentnum}} ], + ); + die "can't send alerter failure email to $failure_recipient: $error" + if $error; + } + +} + +1; diff --git a/FS/FS/Cron/bill.pm b/FS/FS/Cron/bill.pm index ad6498c..dbb6c66 100644 --- a/FS/FS/Cron/bill.pm +++ b/FS/FS/Cron/bill.pm @@ -4,17 +4,26 @@ use strict; use vars qw( @ISA @EXPORT_OK ); use Exporter; use Date::Parse; +use DBI 1.33; #The "clone" method was added in DBI 1.33. use FS::UID qw(dbh); -use FS::Record qw(qsearchs); +use FS::Record qw( qsearch qsearchs ); +use FS::queue; use FS::cust_main; use FS::part_event; use FS::part_event_condition; @ISA = qw( Exporter ); -@EXPORT_OK = qw ( bill ); +@EXPORT_OK = qw ( bill bill_where ); -sub bill { +#freeside-daily %opt: +# -s: re-charge setup fees +# -v: enable debugging +# -l: debugging level +# -m: Experimental multi-process mode uses the job queue for multi-process and/or multi-machine billing. +# -r: Multi-process mode dry run option +# -g: Don't bill these pkgparts +sub bill { my %opt = @_; my $check_freq = $opt{'check_freq'} || '1d'; @@ -22,17 +31,135 @@ sub bill { my $debug = 0; $debug = 1 if $opt{'v'}; $debug = $opt{'l'} if $opt{'l'}; - $FS::cust_main::DEBUG = $debug; #$FS::cust_event::DEBUG = $opt{'l'} if $opt{'l'}; + #we're at now now (and later). + $opt{'time'} = $opt{'d'} ? str2time($opt{'d'}) : $^T; + $opt{'time'} += $opt{'y'} * 86400 if $opt{'y'}; + + $opt{'invoice_time'} = $opt{'n'} ? $^T : $opt{'time'}; + + #hashref here doesn't work with -m + #my $not_pkgpart = $opt{g} ? { map { $_=>1 } split(/,\s*/, $opt{g}) } + # : {}; + + ### + # get a list of custnums + ### + + my $cursor_dbh = dbh->clone; + + $cursor_dbh->do( + "DECLARE cron_bill_cursor CURSOR FOR ". + " SELECT custnum FROM cust_main WHERE ". bill_where( %opt ) + ) or die $cursor_dbh->errstr; + + while ( 1 ) { + + my $sth = $cursor_dbh->prepare('FETCH 100 FROM cron_bill_cursor'); #mysql? + + $sth->execute or die $sth->errstr; + + my @custnums = map { $_->[0] } @{ $sth->fetchall_arrayref }; + + last unless scalar(@custnums); + + ### + # for each custnum, queue or make one customer object and bill + # (one at a time, to reduce memory footprint with large #s of customers) + ### + + foreach my $custnum ( @custnums ) { + + my %args = ( + 'time' => $opt{'time'}, + 'invoice_time' => $opt{'invoice_time'}, + 'actual_time' => $^T, #when freeside-bill was started + #(not, when using -m, freeside-queued) + 'check_freq' => $check_freq, + 'resetup' => ( $opt{'s'} ? $opt{'s'} : 0 ), + 'not_pkgpart' => $opt{'g'}, #$not_pkgpart, + ); + + if ( $opt{'m'} ) { + + if ( $opt{'r'} ) { + warn "DRY RUN: would add custnum $custnum for queued_bill\n"; + } else { + + #avoid queuing another job if there's one still waiting to run + next if qsearch( 'queue', { 'job' => 'FS::cust_main::queued_bill', + 'custnum' => $custnum, + 'status' => 'new', + } + ); + + #add job to queue that calls bill_and_collect with options + my $queue = new FS::queue { + 'job' => 'FS::cust_main::queued_bill', + 'secure' => 'Y', + 'priority' => 99, #don't get in the way of provisioning jobs + }; + my $error = $queue->insert( 'custnum'=>$custnum, %args ); + + } + + } else { + + my $cust_main = qsearchs( 'cust_main', { 'custnum' => $custnum } ); + $cust_main->bill_and_collect( %args, 'debug' => $debug ); + + } + + } + + } + + $cursor_dbh->commit or die $cursor_dbh->errstr; + +} + +# freeside-daily %opt: +# -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). +# +# -n: When used with "-d" and/or "-y", specifies that invoices should be dated +# with today's date, irregardless of the pretend date used to pre-generate +# the invoices. +# +# -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 +# +# -v: enable debugging +# +# -l: debugging level + +sub bill_where { + my( %opt ) = @_; + + my $time = $opt{'time'}; + my $invoice_time = $opt{'invoice_time'}; + + my $check_freq = $opt{'check_freq'} || '1d'; + my @search = (); + push @search, "( cust_main.archived != 'Y' OR archived IS NULL )"; #disable? + push @search, "cust_main.payby = '". $opt{'p'}. "'" if $opt{'p'}; push @search, "cust_main.agentnum = ". $opt{'a'} if $opt{'a'}; + #it would be useful if i recognized $opt{g} / $not_pkgpart... + if ( @ARGV ) { push @search, "( ". join(' OR ', map "cust_main.custnum = $_", @ARGV ). @@ -43,23 +170,22 @@ sub bill { # generate where_pkg/where_event search clause ### - #we're at now now (and later). - my($time)= $opt{'d'} ? str2time($opt{'d'}) : $^T; - $time += $opt{'y'} * 86400 if $opt{'y'}; - - my $invoice_time = $opt{'n'} ? $^T : $time; - # select * from cust_main where my $where_pkg = <<"END"; - 0 < ( select count(*) from cust_pkg - where cust_main.custnum = cust_pkg.custnum - and ( cancel is null or cancel = 0 ) - and ( setup is null or setup = 0 - or bill is null or bill <= $time - or ( expire is not null and expire <= $^T ) - or ( adjourn is not null and adjourn <= $^T ) - ) - ) + EXISTS( + SELECT 1 FROM cust_pkg + WHERE cust_main.custnum = cust_pkg.custnum + AND ( cancel IS NULL OR cancel = 0 ) + AND ( ( ( setup IS NULL OR setup = 0 ) + AND ( start_date IS NULL OR start_date = 0 + OR ( start_date IS NOT NULL AND start_date <= $^T ) + ) + ) + OR bill IS NULL OR bill <= $time + OR ( expire IS NOT NULL AND expire <= $^T ) + OR ( adjourn IS NOT NULL AND adjourn <= $^T ) + ) + ) END my $where_event = join(' OR ', map { @@ -69,23 +195,24 @@ END my $where = FS::part_event_condition->where_conditions_sql( $eventtable, 'time'=>$time, ); + $where = $where ? "AND $where" : ''; my $are_part_event = - "0 < ( SELECT COUNT(*) FROM part_event $join - WHERE check_freq = '$check_freq' - AND eventtable = '$eventtable' - AND ( disabled = '' OR disabled IS NULL ) - AND $where - ) + "EXISTS ( SELECT 1 FROM part_event $join + WHERE check_freq = '$check_freq' + AND eventtable = '$eventtable' + AND ( disabled = '' OR disabled IS NULL ) + $where + ) "; if ( $eventtable eq 'cust_main' ) { $are_part_event; } else { - "0 < ( SELECT COUNT(*) FROM $eventtable - WHERE cust_main.custnum = $eventtable.custnum - AND $are_part_event - ) + "EXISTS ( SELECT 1 FROM $eventtable + WHERE cust_main.custnum = $eventtable.custnum + AND $are_part_event + ) "; } @@ -93,54 +220,11 @@ END push @search, "( $where_pkg OR $where_event )"; - ### - # get a list of custnums - ### - warn "searching for customers:\n". join("\n", @search). "\n" if $opt{'v'} || $opt{'l'}; - my $sth = dbh->prepare( - "SELECT custnum FROM cust_main". - " WHERE ". join(' AND ', @search) - ) or die dbh->errstr; - - $sth->execute or die $sth->errstr; - - my @custnums = map { $_->[0] } @{ $sth->fetchall_arrayref }; - - ### - # for each custnum, queue or make one customer object and bill - # (one at a time, to reduce memory footprint with large #s of customers) - ### - - foreach my $custnum ( @custnums ) { - - my %args = ( - 'time' => $time, - 'invoice_time' => $invoice_time, - 'actual_time' => $^T, #when freeside-bill was started - #(not, when using -m, freeside-queued) - 'check_freq' => $check_freq, - 'resetup' => ( $opt{'s'} ? $opt{'s'} : 0 ), - ); - - if ( $opt{'m'} ) { - - #add job to queue that calls bill_and_collect with options - my $queue = new FS::queue { - 'job' => 'FS::cust_main::queued_bill', - 'secure' => 'Y', - }; - my $error = $queue->insert( 'custnum'=>$custnum, %args ); - - } else { - - my $cust_main = qsearchs( 'cust_main', { 'custnum' => $custnum } ); - $cust_main->bill_and_collect( %args, 'debug' => $debug ); - - } - - } + join(' AND ', @search); } + +1; diff --git a/FS/FS/Cron/check.pm b/FS/FS/Cron/check.pm new file mode 100644 index 0000000..9d3ffbd --- /dev/null +++ b/FS/FS/Cron/check.pm @@ -0,0 +1,200 @@ +package FS::Cron::check; + +use strict; +use vars qw( @ISA @EXPORT_OK $DEBUG $FS_RUN $error_msg + $SELFSERVICE_USER $SELFSERVICE_MACHINES @SELFSERVICE_MACHINES + ); +use Exporter; +use LWP::UserAgent; +use HTTP::Request; +use URI::Escape; +use Email::Send; +use FS::Conf; +use FS::Record qw(qsearch); +use FS::cust_pay_pending; + +@ISA = qw( Exporter ); +@EXPORT_OK = qw( + check_queued check_selfservice check_apache check_bop_failures + check_sg check_sg_login check_sgng + alert error_msg +); + +$DEBUG = 0; + +$FS_RUN = '/var/run'; + +sub check_queued { + _check_fsproc('queued'); +} + +$SELFSERVICE_USER = '%%%SELFSERVICE_USER%%%'; + +$SELFSERVICE_MACHINES = '%%%SELFSERVICE_MACHINES%%%'; #substituted by Makefile +$SELFSERVICE_MACHINES =~ s/^\s+//; +$SELFSERVICE_MACHINES =~ s/\s+$//; +@SELFSERVICE_MACHINES = split(/\s+/, $SELFSERVICE_MACHINES); +@SELFSERVICE_MACHINES = () + if scalar(@SELFSERVICE_MACHINES) == 1 + && $SELFSERVICE_MACHINES[0] eq '%%%'.'SELFSERVICE_MACHINES'.'%%%'; + +sub check_selfservice { + foreach my $machine ( @SELFSERVICE_MACHINES ) { + unless ( _check_fsproc("selfservice-server.$SELFSERVICE_USER.$machine") ) { + $error_msg = "Self-service daemon not running for $machine"; + return 0; + } + } + return 1; +} + +sub check_sg { + my $conf = new FS::Conf; + #different trigger if they ever stop using multicustomer_hack ? + return 1 unless $conf->exists('sg-multicustomer_hack'); + + my $ua = new LWP::UserAgent; + $ua->agent("FreesideCronCheck/0.1 " . $ua->agent); + + my $USER = $conf->config('sg-ping_username'); + my $PASS = $conf->config('sg-ping_password'); + my $req = new HTTP::Request GET=>"https://$USER:$PASS\@localhost/sg/ping.cgi"; + my $res = $ua->request($req); + + return 1 if $res->is_success + && $res->content =~ /OK/ + && $res->content !~ /error/i; #doh, the error message includes "OK" + + $error_msg = $res->is_success ? $res->content : $res->status_line; + return 0; +} + +sub check_sg_login { + my $conf = new FS::Conf; + #different trigger if they ever stop using multicustomer_hack ? + return 1 unless $conf->exists('sg-multicustomer_hack'); + + my $ua = new LWP::UserAgent; + $ua->agent("FreesideCronCheck/0.1 " . $ua->agent); + + my $USER = $conf->config('sg-ping_username'); + my $PASS = $conf->config('sg-ping_password'); + my $USERNAME = $conf->config('sg-login_username'); + my $req = new HTTP::Request + GET=>"https://$USER:$PASS\@localhost/sg/start.cgi?". + 'username='. uri_escape($USERNAME); + my $res = $ua->request($req); + + return 1 if $res->is_success + && $res->content =~ /[\da-f]{32}/i #session_id + && $res->content !~ /error/i; + + $error_msg = $res->is_success ? $res->content : $res->status_line; + return 0; +} + +sub check_sgng { + my $conf = new FS::Conf; + #different trigger if they ever stop using multicustomer_hack ? + return 1 unless $conf->exists('sg-multicustomer_hack'); + + eval 'use RPC::XML; use RPC::XML::Client;'; + if ($@) { $error_msg = $@; return 0; }; + + my $cli = RPC::XML::Client->new('https://localhost/selfservice/xmlrpc.cgi'); + my $resp = $cli->send_request('FS.SelfService.XMLRPC.ping'); + + return 1 if ref($resp) + && ! $resp->is_fault + && ref($resp->value) + && $resp->value->{'pong'} == 1; + + #hua + $error_msg = ref($resp) + ? ( $resp->is_fault + ? $resp->string + : ( ref($resp->value) ? $resp->value->{'error'} + : $resp->value + ) + ) + : $resp; + return 0; +} + +sub _check_fsproc { + my $arg = shift; + _check_pidfile( "freeside-$arg.pid" ); +} + +sub _check_pidfile { + my $pidfile = shift; + open(PID, "$FS_RUN/$pidfile") or return 0; + chomp( my $pid = scalar(<PID>) ); + close PID; # or return 0; + + $pid && kill 0, $pid; +} + +sub check_apache { + my $ua = new LWP::UserAgent; + $ua->agent("FreesideCronCheck/0.1 " . $ua->agent); + + my $req = new HTTP::Request GET => 'https://localhost/'; + my $res = $ua->request($req); + + return 1 if $res->is_success || $res->status_line =~ /^403/; + $error_msg = $res->status_line; + return 0; + +} + +#and now for something entirely different... +my $num_consecutive_bop_failures = 60; +sub check_bop_failures { + + return 1 if grep { $_->statustext eq 'captured' } + qsearch({ + 'table' => 'cust_pay_pending', + 'hashref' => { 'status' => 'done' }, + 'order_by' => 'ORDER BY paypendingnum DESC'. + " LIMIT $num_consecutive_bop_failures", + }); + $error_msg = "Last $num_consecutive_bop_failures real-time payments failed"; + return 0; +} + +# + +sub error_msg { + $error_msg; +} + +sub alert { + my( $alert, @emails ) = @_; + + my $conf = new FS::Conf; + my $smtpmachine = $conf->config('smtpmachine'); + my $company_name = $conf->config('company_name'); + + foreach my $email (@emails) { + warn "warning $email about $alert\n" if $DEBUG; + + my $message = <<"__MESSAGE__"; +From: support\@freeside.biz +To: $email +Subject: FREESIDE ALERT for $company_name + +FREESIDE ALERT: $alert + +__MESSAGE__ + + my $sender = Email::Send->new({ mailer => 'SMTP' }); + $sender->mailer_args([ Host => $smtpmachine ]); + $sender->send($message); + + } + +} + +1; + diff --git a/FS/FS/Cron/notify.pm b/FS/FS/Cron/notify.pm index 23cf920..5b0e186 100644 --- a/FS/FS/Cron/notify.pm +++ b/FS/FS/Cron/notify.pm @@ -35,7 +35,7 @@ sub notify_flat_delay { and 0 < ( select count(*) from part_pkg_option where part_pkg.pkgpart = part_pkg_option.pkgpart and part_pkg_option.optionname = 'recur_notify' - and part_pkg_option.optionvalue > 0 + and CAST( part_pkg_option.optionvalue AS INTEGER ) > 0 and 0 <= ( $time + CAST( part_pkg_option.optionvalue AS $integer ) * 86400 @@ -62,7 +62,7 @@ END 0 = ( select count(*) from cust_pkg_option where cust_pkg.pkgnum = cust_pkg_option.pkgnum and cust_pkg_option.optionname = 'impending_recur_notification_sent' - and cust_pkg_option.optionvalue = 1 + and CAST( cust_pkg_option.optionvalue AS INTEGER ) = 1 ) END diff --git a/FS/FS/Cron/upload.pm b/FS/FS/Cron/upload.pm new file mode 100644 index 0000000..fea3d2c --- /dev/null +++ b/FS/FS/Cron/upload.pm @@ -0,0 +1,176 @@ +package FS::Cron::upload; + +use strict; +use vars qw( @ISA @EXPORT_OK $me $DEBUG ); +use Exporter; +use Date::Format; +use FS::UID qw(dbh); +use FS::Record qw( qsearch qsearchs ); +use FS::Conf; +use FS::queue; +use FS::agent; +use LWP::UserAgent; +use HTTP::Request; +use HTTP::Request::Common; +use HTTP::Response; + +@ISA = qw( Exporter ); +@EXPORT_OK = qw ( upload ); +$DEBUG = 0; +$me = '[FS::Cron::upload]'; + +#freeside-daily %opt: +# -v: enable debugging +# -l: debugging level +# -m: Experimental multi-process mode uses the job queue for multi-process and/or multi-machine billing. +# -r: Multi-process mode dry run option +# -a: Only process customers with the specified agentnum + + +sub upload { + my %opt = @_; + + my $debug = 0; + $debug = 1 if $opt{'v'}; + $debug = $opt{'l'} if $opt{'l'}; + + local $DEBUG = $debug if $debug; + + warn "$me upload called\n" if $DEBUG; + + my $conf = new FS::Conf; + my @agent = grep { $conf->config( 'billco-username', $_->agentnum, 1 ) } + grep { $conf->config( 'billco-password', $_->agentnum, 1 ) } + qsearch( 'agent', {} ); + + my $date = time2str('%Y%m%d%H%M%S', $^T); # more? + + @agent = grep { $_ == $opt{'a'} } @agent if $opt{'a'}; + + foreach my $agent ( @agent ) { + + my $agentnum = $agent->agentnum; + + if ( $opt{'m'} ) { + + if ( $opt{'r'} ) { + warn "DRY RUN: would add agent $agentnum for queued upload\n"; + } else { + + my $queue = new FS::queue { + 'job' => 'FS::Cron::upload::billco_upload', + }; + my $error = $queue->insert( + 'agentnum' => $agentnum, + 'date' => $date, + 'l' => $opt{'l'} || '', + 'm' => $opt{'m'} || '', + 'v' => $opt{'v'} || '', + ); + + } + + } else { + + eval "&billco_upload( 'agentnum' => $agentnum, 'date' => $date );"; + warn "billco_upload failed: $@\n" + if ( $@ ); + + } + + } + +} + +sub billco_upload { + my %opt = @_; + + warn "$me billco_upload called\n" if $DEBUG; + my $conf = new FS::Conf; + my $dir = '%%%FREESIDE_EXPORT%%%/export.'. $FS::UID::datasrc. '/cust_bill'; + + my $agentnum = $opt{agentnum} or die "no agentnum provided\n"; + my $url = $conf->config( 'billco-url', $agentnum ) + or die "no url for agent $agentnum\n"; + my $username = $conf->config( 'billco-username', $agentnum, 1 ) + or die "no username for agent $agentnum\n"; + my $password = $conf->config( 'billco-password', $agentnum, 1 ) + or die "no password for agent $agentnum\n"; + my $clicode = $conf->config( 'billco-clicode', $agentnum ) + or die "no clicode for agent $agentnum\n"; + + die "no date provided\n" unless $opt{date}; + my $zipfile = "$dir/agentnum$agentnum-$opt{date}.zip"; + + 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 $agent = qsearchs( 'agent', { agentnum => $agentnum } ) + or die "no such agent: $agentnum"; + $agent->select_for_update; #mutex + + unless ( -f "$dir/agentnum$agentnum-header.csv" || + -f "$dir/agentnum$agentnum-detail.csv" ) + { + warn "$me neither $dir/agentnum$agentnum-header.csv nor ". + "$dir/agentnum$agentnum-detail.csv found\n" if $DEBUG; + $dbh->commit or die $dbh->errstr if $oldAutoCommit; + return; + } + + # a better way? + if ($opt{m}) { + my $sql = "SELECT count(*) FROM queue LEFT JOIN cust_main USING(custnum) ". + "WHERE queue.job='FS::cust_main::queued_bill' AND cust_main.agentnum = ?"; + my $sth = $dbh->prepare($sql) or die $dbh->errstr; + while (1) { + $sth->execute( $agentnum ) + or die "Unexpected error executing statement $sql: ". $sth->errstr; + last if $sth->fetchow_arrayref->[0]; + sleep 300; + } + } + + foreach ( qw ( header detail ) ) { + rename "$dir/agentnum$agentnum-$_.csv", + "$dir/agentnum$agentnum-$opt{date}-$_.csv"; + } + + my $command = "cd $dir; zip $zipfile ". + "agentnum$agentnum-$opt{date}-header.csv ". + "agentnum$agentnum-$opt{date}-detail.csv"; + + system($command) and die "$command failed\n"; + + unlink "agentnum$agentnum-$opt{date}-header.csv", + "agentnum$agentnum-$opt{date}-detail.csv"; + + my $ua = new LWP::UserAgent; + my $res = $ua->request( POST( $url, + 'Content_Type' => 'form-data', + 'Content' => [ 'username' => $username, + 'pass' => $password, + 'custid' => $username, + 'clicode' => $clicode, + 'file1' => [ $zipfile ], + ], + ) + ); + + die "upload failed: ". $res->status_line. "\n" + unless $res->is_success; + + $dbh->commit or die $dbh->errstr if $oldAutoCommit; + ''; + +} + +1; |