don't queue spool_upload jobs until all queued_bill jobs are finished, #6802
[freeside.git] / FS / FS / Cron / upload.pm
1 package FS::Cron::upload;
2
3 use strict;
4 use vars qw( @ISA @EXPORT_OK $me $DEBUG );
5 use Exporter;
6 use Date::Format;
7 use FS::UID qw(dbh);
8 use FS::Record qw( qsearch qsearchs );
9 use FS::Conf;
10 use FS::queue;
11 use FS::agent;
12 use LWP::UserAgent;
13 use HTTP::Request;
14 use HTTP::Request::Common;
15 use HTTP::Response;
16 use Net::FTP;
17
18 @ISA = qw( Exporter );
19 @EXPORT_OK = qw ( upload );
20 $DEBUG = 0;
21 $me = '[FS::Cron::upload]';
22
23 #freeside-daily %opt:
24 #  -v: enable debugging
25 #  -l: debugging level
26 #  -m: Experimental multi-process mode uses the job queue for multi-process and/or multi-machine billing.
27 #  -r: Multi-process mode dry run option
28 #  -a: Only process customers with the specified agentnum
29
30
31 sub upload {
32   my %opt = @_;
33
34   my $debug = 0;
35   $debug = 1 if $opt{'v'};
36   $debug = $opt{'l'} if $opt{'l'};
37
38   local $DEBUG = $debug if $debug;
39
40   warn "$me upload called\n" if $DEBUG;
41
42   my @tasks;
43
44   my $date =  time2str('%Y%m%d%H%M%S', $^T); # more?
45
46   my $conf = new FS::Conf;
47
48   my @agents = $opt{'a'} ? FS::agent->by_key($opt{'a'}) : qsearch('agent', {});
49
50   if ( $conf->exists('cust_bill-ftp_spool') ) {
51     my $url = $conf->config('cust_bill-ftpdir');
52     $url = "/$url" unless $url =~ m[^/];
53     $url = 'ftp://' . $conf->config('cust_bill-ftpserver') . $url;
54
55     my $format = $conf->config('cust_bill-ftpformat');
56     my $username = $conf->config('cust_bill-ftpusername');
57     my $password = $conf->config('cust_bill-ftppassword');
58
59     my %task = (
60       'date'      => $date,
61       'l'         => $opt{'l'},
62       'm'         => $opt{'m'},
63       'v'         => $opt{'v'},
64       'username'  => $username,
65       'password'  => $password,
66       'url'       => $url,
67       'format'    => $format,
68     );
69
70     if ( $conf->exists('cust_bill-spoolagent') ) {
71       # then push each agent's spool separately
72       foreach ( @agents ) {
73         push @tasks, { %task, 'agentnum' => $_->agentnum };
74       }
75     }
76     elsif ( $opt{'a'} ) {
77       warn "Per-agent processing, but cust_bill-spoolagent is not enabled.\nSkipped invoice upload.\n";
78     }
79     else {
80       push @tasks, \%task;
81     }
82   }
83
84   else { #check each agent for billco upload settings
85
86     my %task = (
87       'date'      => $date,
88       'l'         => $opt{'l'},
89       'm'         => $opt{'m'},
90       'v'         => $opt{'v'},
91     );
92
93     foreach (@agents) {
94       my $agentnum = $_->agentnum;
95
96       if ( $conf->config( 'billco-username', $agentnum, 1 ) ) {
97         my $username = $conf->config('billco-username', $agentnum, 1);
98         my $password = $conf->config('billco-password', $agentnum, 1);
99         my $clicode  = $conf->config('billco-clicode',  $agentnum, 1);
100         my $url      = $conf->config('billco-url',      $agentnum);
101         push @tasks, {
102           %task,
103           'agentnum' => $agentnum,
104           'username' => $username,
105           'password' => $password,
106           'url'      => $url,
107           'clicode'  => $clicode,
108           'format'   => 'billco',
109         };
110       }
111     } # foreach @agents
112
113   } #!if cust_bill-ftp_spool
114
115   # if there's nothing to do, don't hold up the rest of the process
116   return '' if !@tasks;
117
118   # wait for any ongoing billing jobs to complete
119   if ($opt{m}) {
120     my $dbh = dbh;
121     my $sql = "SELECT count(*) FROM queue LEFT JOIN cust_main USING(custnum) ".
122     "WHERE queue.job='FS::cust_main::queued_bill' AND status != 'failed'";
123     if (@agents) {
124       $sql .= ' AND cust_main.agentnum IN('.
125         join(',', map {$_->agentnum} @agents).
126         ')';
127     }
128     my $sth = $dbh->prepare($sql) or die $dbh->errstr;
129     while (1) {
130       $sth->execute()
131         or die "Unexpected error executing statement $sql: ". $sth->errstr;
132       last if $sth->fetchrow_arrayref->[0] == 0;
133       warn "Waiting 5min for billing to complete...\n" if $DEBUG;
134       sleep 300;
135     }
136   }
137
138   foreach (@tasks) {
139
140     my $agentnum = $_->{agentnum};
141
142     if ( $opt{'m'} ) {
143
144       if ( $opt{'r'} ) {
145         warn "DRY RUN: would add agent $agentnum for queued upload\n";
146       } else {
147         my $queue = new FS::queue {
148           'job'      => 'FS::Cron::upload::spool_upload',
149         };
150         my $error = $queue->insert( %$_ );
151       }
152
153     } else {
154
155       eval { spool_upload(%$_) };
156       warn "spool_upload failed: $@\n"
157         if $@;
158
159     }
160
161   }
162
163 }
164
165 sub spool_upload {
166   my %opt = @_;
167
168   warn "$me spool_upload called\n" if $DEBUG;
169   my $conf = new FS::Conf;
170   my $dir = '%%%FREESIDE_EXPORT%%%/export.'. $FS::UID::datasrc. '/cust_bill';
171
172   my $agentnum = $opt{agentnum} || '';
173   my $url      = $opt{url} or die "no url for agent $agentnum\n";
174   $url =~ s/^\s+//; $url =~ s/\s+$//;
175
176   my $username = $opt{username} or die "no username for agent $agentnum\n";
177   my $password = $opt{password} or die "no password for agent $agentnum\n";
178
179   die "no date provided\n" unless $opt{date};
180
181   local $SIG{HUP} = 'IGNORE';
182   local $SIG{INT} = 'IGNORE';
183   local $SIG{QUIT} = 'IGNORE';
184   local $SIG{TERM} = 'IGNORE';
185   local $SIG{TSTP} = 'IGNORE';
186   local $SIG{PIPE} = 'IGNORE';
187
188   my $oldAutoCommit = $FS::UID::AutoCommit;
189   local $FS::UID::AutoCommit = 0;
190   my $dbh = dbh;
191
192   if ( $agentnum ) {
193     my $agent = qsearchs( 'agent', { agentnum => $agentnum } )
194       or die "no such agent: $agentnum";
195     $agent->select_for_update; #mutex 
196   }
197
198   if ( $opt{'format'} eq 'billco' ) {
199
200     die "no agentnum provided\n" unless $agentnum;
201
202     my $zipfile  = "$dir/agentnum$agentnum-$opt{date}.zip";
203
204     unless ( -f "$dir/agentnum$agentnum-header.csv" ||
205              -f "$dir/agentnum$agentnum-detail.csv" )
206     {
207       warn "$me neither $dir/agentnum$agentnum-header.csv nor ".
208            "$dir/agentnum$agentnum-detail.csv found\n" if $DEBUG;
209       $dbh->commit or die $dbh->errstr if $oldAutoCommit;
210       return;
211     }
212
213     foreach ( qw ( header detail ) ) {
214       rename "$dir/agentnum$agentnum-$_.csv",
215              "$dir/agentnum$agentnum-$opt{date}-$_.csv";
216     }
217
218     my $command = "cd $dir; zip $zipfile ".
219                   "agentnum$agentnum-$opt{date}-header.csv ".
220                   "agentnum$agentnum-$opt{date}-detail.csv";
221
222     system($command) and die "$command failed\n";
223
224     unlink "agentnum$agentnum-$opt{date}-header.csv",
225            "agentnum$agentnum-$opt{date}-detail.csv";
226
227     if ( $url =~ /^http/i ) {
228
229       my $ua = new LWP::UserAgent;
230       my $res = $ua->request( POST( $url,
231                                     'Content_Type' => 'form-data',
232                                     'Content' => [ 'username' => $username,
233                                                    'pass'     => $password,
234                                                    'custid'   => $username,
235                                                    'clicode'  => $opt{clicode},
236                                                    'file1'    => [ $zipfile ],
237                                                  ],
238                                   )
239                             );
240
241       die "upload failed: ". $res->status_line. "\n"
242         unless $res->is_success;
243
244     } elsif ( $url =~ /^ftp:\/\/([\w\.]+)(\/.*)$/i ) {
245
246       my($hostname, $path) = ($1, $2);
247
248       my $ftp = new Net::FTP($hostname, Passive=>1)
249         or die "can't connect to $hostname: $@\n";
250       $ftp->login($username, $password)
251         or die "can't login to $hostname: ". $ftp->message."\n";
252       unless ( $ftp->cwd($path) ) {
253         my $msg = "can't cd $path on $hostname: ". $ftp->message. "\n";
254         ( $path eq '/' ) ? warn $msg : die $msg;
255       }
256       $ftp->binary
257         or die "can't set binary mode on $hostname\n";
258
259       $ftp->put($zipfile)
260         or die "can't put $zipfile: ". $ftp->message. "\n";
261
262       $ftp->quit;
263
264     } else {
265       die "unknown scheme in URL $url\n";
266     }
267
268   } else { #$opt{format} ne 'billco'
269
270     my $date = $opt{date};
271     my $file = $opt{agentnum} ? "agentnum$opt{agentnum}" : 'spool'; #.csv
272     unless ( -f "$dir/$file.csv" ) {
273       warn "$me $dir/$file.csv not found\n" if $DEBUG;
274       $dbh->commit or die $dbh->errstr if $oldAutoCommit;
275       return;
276     }
277     rename "$dir/$file.csv", "$dir/$file-$date.csv";
278
279     #ftp only for now
280     if ( $url =~ m{^ftp://([\w\.]+)(/.*)$}i ) {
281
282       my ($hostname, $path) = ($1, $2);
283       my $ftp = new Net::FTP ($hostname)
284         or die "can't connect to $hostname: $@\n";
285       $ftp->login($username, $password)
286         or die "can't login to $hostname: ".$ftp->message."\n";
287       unless ( $ftp->cwd($path) ) {
288         my $msg = "can't cd $path on $hostname: ".$ftp->message."\n";
289         ( $path eq '/' ) ? warn $msg : die $msg;
290       }
291       chdir($dir);
292       $ftp->put("$file-$date.csv")
293         or die "can't put $file-$date.csv: ".$ftp->message."\n";
294       $ftp->quit;
295
296     } else {
297       die "malformed FTP URL $url\n";
298     }
299   } #opt{format}
300
301   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
302   '';
303
304 }
305
306 1;