non-billco FTP invoice spool upload, #16382
[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   foreach (@tasks) {
116
117     my $agentnum = $_->{agentnum};
118
119     if ( $opt{'m'} ) {
120
121       if ( $opt{'r'} ) {
122         warn "DRY RUN: would add agent $agentnum for queued upload\n";
123       } else {
124         my $queue = new FS::queue {
125           'job'      => 'FS::Cron::upload::spool_upload',
126         };
127         my $error = $queue->insert( %$_ );
128       }
129
130     } else {
131
132       eval { spool_upload(%$_) };
133       warn "spool_upload failed: $@\n"
134         if $@;
135
136     }
137
138   }
139
140 }
141
142 sub spool_upload {
143   my %opt = @_;
144
145   warn "$me spool_upload called\n" if $DEBUG;
146   my $conf = new FS::Conf;
147   my $dir = '%%%FREESIDE_EXPORT%%%/export.'. $FS::UID::datasrc. '/cust_bill';
148
149   my $agentnum = $opt{agentnum} or die "no agentnum provided\n";
150   my $url      = $opt{url} or die "no url for agent $agentnum\n";
151   $url =~ s/^\s+//; $url =~ s/\s+$//;
152
153   my $username = $opt{username} or die "no username for agent $agentnum\n";
154   my $password = $opt{password} or die "no password for agent $agentnum\n";
155
156   die "no date provided\n" unless $opt{date};
157
158   local $SIG{HUP} = 'IGNORE';
159   local $SIG{INT} = 'IGNORE';
160   local $SIG{QUIT} = 'IGNORE';
161   local $SIG{TERM} = 'IGNORE';
162   local $SIG{TSTP} = 'IGNORE';
163   local $SIG{PIPE} = 'IGNORE';
164
165   my $oldAutoCommit = $FS::UID::AutoCommit;
166   local $FS::UID::AutoCommit = 0;
167   my $dbh = dbh;
168
169   my $agent = qsearchs( 'agent', { agentnum => $agentnum } )
170     or die "no such agent: $agentnum";
171   $agent->select_for_update; #mutex 
172
173   if ( $opt{'format'} eq 'billco' ) {
174
175     my $zipfile  = "$dir/agentnum$agentnum-$opt{date}.zip";
176
177     unless ( -f "$dir/agentnum$agentnum-header.csv" ||
178              -f "$dir/agentnum$agentnum-detail.csv" )
179     {
180       warn "$me neither $dir/agentnum$agentnum-header.csv nor ".
181            "$dir/agentnum$agentnum-detail.csv found\n" if $DEBUG;
182       $dbh->commit or die $dbh->errstr if $oldAutoCommit;
183       return;
184     }
185
186     # a better way?
187     if ($opt{m}) {
188       my $sql = "SELECT count(*) FROM queue LEFT JOIN cust_main USING(custnum) ".
189         "WHERE queue.job='FS::cust_main::queued_bill' AND cust_main.agentnum = ?";
190       my $sth = $dbh->prepare($sql) or die $dbh->errstr;
191       while (1) {
192         $sth->execute( $agentnum )
193           or die "Unexpected error executing statement $sql: ". $sth->errstr;
194         last if $sth->fetchrow_arrayref->[0];
195         sleep 300;
196       }
197     }
198
199     foreach ( qw ( header detail ) ) {
200       rename "$dir/agentnum$agentnum-$_.csv",
201              "$dir/agentnum$agentnum-$opt{date}-$_.csv";
202     }
203
204     my $command = "cd $dir; zip $zipfile ".
205                   "agentnum$agentnum-$opt{date}-header.csv ".
206                   "agentnum$agentnum-$opt{date}-detail.csv";
207
208     system($command) and die "$command failed\n";
209
210     unlink "agentnum$agentnum-$opt{date}-header.csv",
211            "agentnum$agentnum-$opt{date}-detail.csv";
212
213     if ( $url =~ /^http/i ) {
214
215       my $ua = new LWP::UserAgent;
216       my $res = $ua->request( POST( $url,
217                                     'Content_Type' => 'form-data',
218                                     'Content' => [ 'username' => $username,
219                                                    'pass'     => $password,
220                                                    'custid'   => $username,
221                                                    'clicode'  => $opt{clicode},
222                                                    'file1'    => [ $zipfile ],
223                                                  ],
224                                   )
225                             );
226
227       die "upload failed: ". $res->status_line. "\n"
228         unless $res->is_success;
229
230     } elsif ( $url =~ /^ftp:\/\/([\w\.]+)(\/.*)$/i ) {
231
232       my($hostname, $path) = ($1, $2);
233
234       my $ftp = new Net::FTP($hostname)
235         or die "can't connect to $hostname: $@\n";
236       $ftp->login($username, $password)
237         or die "can't login to $hostname: ". $ftp->message."\n";
238       unless ( $ftp->cwd($path) ) {
239         my $msg = "can't cd $path on $hostname: ". $ftp->message. "\n";
240         ( $path eq '/' ) ? warn $msg : die $msg;
241       }
242       $ftp->binary
243         or die "can't set binary mode on $hostname\n";
244
245       $ftp->put($zipfile)
246         or die "can't put $zipfile: ". $ftp->message. "\n";
247
248       $ftp->quit;
249
250     } else {
251       die "unknown scheme in URL $url\n";
252     }
253
254   } else { #$opt{format} ne 'billco'
255
256     my $date = $opt{date};
257     my $file = $opt{agentnum} ? "agentnum$opt{agentnum}" : 'spool'; #.csv
258     unless ( -f "$dir/$file.csv" ) {
259       warn "$me $dir/$file.csv not found\n" if $DEBUG;
260       $dbh->commit or die $dbh->errstr if $oldAutoCommit;
261       return;
262     }
263     rename "$dir/$file.csv", "$dir/$file-$date.csv";
264
265     #ftp only for now
266     if ( $url =~ m{^ftp://([\w\.]+)(/.*)$}i ) {
267
268       my ($hostname, $path) = ($1, $2);
269       my $ftp = new Net::FTP ($hostname)
270         or die "can't connect to $hostname: $@\n";
271       $ftp->login($username, $password)
272         or die "can't login to $hostname: ".$ftp->message."\n";
273       unless ( $ftp->cwd($path) ) {
274         my $msg = "can't cd $path on $hostname: ".$ftp->message."\n";
275         ( $path eq '/' ) ? warn $msg : die $msg;
276       }
277       chdir($dir);
278       $ftp->put("$file-$date.csv")
279         or die "can't put $file-$date.csv: ".$ftp->message."\n";
280       $ftp->quit;
281
282     } else {
283       die "malformed FTP URL $url\n";
284     }
285   } #opt{format}
286
287   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
288   '';
289
290 }
291
292 1;