default to a session cookie instead of setting an explicit timeout, weird timezone...
[freeside.git] / FS / bin / freeside-ipifony-download
1 #!/usr/bin/perl
2
3 use strict;
4 use Getopt::Std;
5 use Date::Format qw(time2str);
6 use File::Temp qw(tempdir);
7 use Net::SFTP::Foreign;
8 use File::Copy qw(copy);
9 use Text::CSV;
10 use FS::UID qw(adminsuidsetup);
11 use FS::Record qw(qsearch qsearchs);
12 use FS::cust_main;
13 use FS::Conf;
14 use FS::Log;
15
16 our %opt;
17 getopts('vqNa:P:C:e:', \%opt);
18
19 # Product codes that are subject to flat rate E911 charges.  For these 
20 # products, the'quantity' field represents the number of lines.
21 my @E911_CODES = ( 'V-HPBX', 'V-TRUNK' );
22
23 # Map TAXNONVOICE/TAXVOICE to Freeside taxclass names
24 my %TAXCLASSES = (
25   'TAXNONVOICE' => 'Other',
26   'TAXVOICE'   => 'VoIP',
27 );
28   
29
30 #$Net::SFTP::Foreign::debug = -1;
31 sub HELP_MESSAGE { '
32   Usage:
33       freeside-ipifony-download 
34         [ -v ]
35         [ -q ]
36         [ -N ]
37         [ -a archivedir ]
38         [ -P port ]
39         [ -C category ]
40         [ -e pkgpart ]
41         freesideuser sftpuser@hostname[:path]
42 ' }
43
44 my @fields = (
45   'custnum',
46   'date_desc',
47   'quantity',
48   'unit_price',
49   'classname',
50   'taxclass',
51 );
52
53 my $user = shift or die &HELP_MESSAGE;
54 my $dbh = adminsuidsetup $user;
55 $FS::UID::AutoCommit = 0;
56
57 # for statistics
58 my $num_charges = 0;
59 my $num_errors = 0;
60 my $sum_charges = 0;
61 # cache classnums
62 my %classnum_of;
63
64 if ( $opt{a} ) {
65   die "no such directory: $opt{a}\n"
66     unless -d $opt{a};
67   die "archive directory $opt{a} is not writable by the freeside user\n"
68     unless -w $opt{a};
69 }
70
71 my $e911_part_pkg;
72 if ( $opt{e} ) {
73   $e911_part_pkg = FS::part_pkg->by_key($opt{e})
74     or die "E911 pkgpart $opt{e} not found.\n";
75
76   if ( $e911_part_pkg->base_recur > 0 or $e911_part_pkg->freq ) {
77     die "E911 pkgpart $opt{e} must be a one-time charge.\n";
78   }
79 }
80
81 my $categorynum = '';
82 if ( $opt{C} ) {
83   # find this category (don't auto-create it, it should exist already)
84   my $category = qsearchs('pkg_category', { categoryname => $opt{C} });
85   if (!defined($category)) {
86     die "Package category '$opt{C}' does not exist.\n";
87   }
88   $categorynum = $category->categorynum;
89 }
90
91 #my $tmpdir = File::Temp->newdir();
92 my $tmpdir = tempdir( CLEANUP => 1 ); #DIR=>somewhere?
93
94 my $host = shift
95   or die &HELP_MESSAGE;
96 my ($sftpuser, $path);
97 $host =~ s/^(.+)\@//;
98 $sftpuser = $1 || $ENV{USER};
99 $host =~ s/:(.*)//;
100 $path = $1;
101
102 my $port = 22;
103 if ( $opt{P} =~ /^(\d+)$/ ) {
104   $port = $1;
105 }
106
107 # for now assume SFTP download as the only method
108 my $sftp = sftp_connect($host, $sftpuser, $port);
109 if ( $sftp->error ) {
110   my $error = "Connection failed to $sftpuser\@$host: ". $sftp->error.
111               ", giving up.";
112   mylog('critical', $error);
113   die $error;
114 }
115
116 $sftp->setcwd($path) if $path;
117
118 my $files = $sftp->ls('ready', wanted => qr/\.csv$/, names_only => 1);
119 if (!@$files) {
120   mylog('warning',"No charge files found.");
121   exit(-1);
122 }
123
124 my %cust_main; # cache
125 my %e911_qty; # custnum => sum of E911-subject quantity
126
127 my %is_e911 = map {$_ => 1} @E911_CODES;
128
129 FILE: foreach my $filename (@$files) {
130   mylog('debug', "Retrieving $filename");
131   $sftp->get("ready/$filename", "$tmpdir/$filename");
132   if($sftp->error) {
133     warn "failed to download $filename\n";
134     next FILE;
135   }
136
137   # make sure server archive dir exists
138   if ( !$sftp->stat('done') ) {
139     mylog('debug',"Creating $path/done");
140     $sftp->mkdir('done');
141     if($sftp->error) {
142       # something is seriously wrong
143       die "failed to create archive directory on server:\n".$sftp->error."\n";
144     }
145   }
146   #move to server archive dir
147   $sftp->rename("ready/$filename", "done/$filename");
148   if($sftp->error) {
149     warn "failed to archive $filename on server:\n".$sftp->error."\n";
150   } # process it anyway, I guess/
151
152   #copy to local archive dir
153   if ( $opt{a} ) {
154     mylog('debug', "Copying $tmpdir/$filename to archive dir $opt{a}");
155     copy("$tmpdir/$filename", $opt{a});
156     #log too?  what's -a all about anyway?
157     warn "failed to copy $tmpdir/$filename to $opt{a}: $!" if $!;
158   }
159
160   open my $fh, "<$tmpdir/$filename";
161   my $csv = Text::CSV->new; # orthodox CSV
162   my %hash;
163   while (my $line = <$fh>) {
164     $csv->parse($line) or do {
165       warn "can't parse $filename: ".$csv->error_input."\n";
166       next FILE;
167     };
168     @hash{@fields} = $csv->fields();
169     if ( $hash{custnum} =~ /^cust/ ) {
170       # there appears to be a header row
171       mylog('debug', "skipping header row");
172       next;
173     }
174     my $cust_main = 
175       $cust_main{$hash{custnum}} ||= FS::cust_main->by_key($hash{custnum});
176     if (!$cust_main) {
177       warn "customer #$hash{custnum} not found\n";
178       next;
179     }
180     mylog('debug',"Found customer #$hash{custnum}: ".$cust_main->name);
181
182     my $amount = sprintf('%.2f',$hash{quantity} * $hash{unit_price});
183
184     # bill the charge on the customer's next bill date, if that's within
185     # the current calendar month; otherwise bill it immediately
186     # (see RT#24325)
187     my $next_bill_date = $cust_main->next_bill_date;
188     if ( $next_bill_date ) {
189       my ($bill_month, $bill_year) = (localtime($next_bill_date))[4, 5];
190       my ($this_month, $this_year) = (localtime(time))[4, 5];
191       if ( $opt{N} or 
192            $this_month == $bill_month and $this_year == $bill_year ) {
193         $cust_main->set('charge_date', $next_bill_date);
194       }
195     }
196
197     # construct arguments for $cust_main->charge
198     my %charge_opt = (
199       amount      => $hash{unit_price},
200       quantity    => $hash{quantity},
201       start_date  => $cust_main->get('charge_date'),
202       pkg         => $hash{date_desc},
203       taxclass    => $TAXCLASSES{ $hash{taxclass} },
204     );
205     if ( $opt{q} ) {
206       $charge_opt{pkg} .= ' (' . $hash{quantity} . ' @ $' . $hash{unit_price} . ' ea)';
207     }
208     if (my $classname = $hash{classname}) {
209       if (!exists($classnum_of{$classname}) ) {
210         # then look it up
211         my $pkg_class = qsearchs('pkg_class', {
212             classname   => $classname,
213             categorynum => $categorynum,
214         });
215         if (!defined($pkg_class)) {
216           # then create it
217           $pkg_class = FS::pkg_class->new({
218               classname   => $classname,
219               categorynum => $categorynum,
220           });
221           my $error = $pkg_class->insert;
222           die "Error creating package class for product code '$classname':\n".
223             "$error\n"
224             if $error;
225         }
226
227         $classnum_of{$classname} = $pkg_class->classnum;
228       }
229       $charge_opt{classnum} = $classnum_of{$classname};
230     }
231     mylog('debug', "  Charging $hash{unit_price} * $hash{quantity}");
232     my $error = $cust_main->charge(\%charge_opt);
233     if ($error) {
234       warn "Error creating charge: $error" if $error;
235       $num_errors++;
236     } else {
237       $num_charges++;
238       $sum_charges += $amount;
239     }
240
241     if ( $opt{e} and $is_e911{$hash{classname}} ) {
242       $e911_qty{$hash{custnum}} ||= 0;
243       $e911_qty{$hash{custnum}} += $hash{quantity};
244     }
245   } #while $line
246   close $fh;
247 } #FILE
248
249 # Order E911 packages
250 my $num_e911 = 0;
251 my $num_lines = 0;
252 foreach my $custnum ( keys (%e911_qty) ) {
253   my $cust_main = $cust_main{$custnum};
254   my $quantity = $e911_qty{$custnum};
255   next if $quantity == 0;
256   my $cust_pkg = FS::cust_pkg->new({
257       pkgpart     => $opt{e},
258       custnum     => $custnum,
259       start_date  => $cust_main->get('charge_date'),
260       quantity    => $quantity,
261   });
262   my $error = $cust_main->order_pkg({ cust_pkg => $cust_pkg });
263   if ( $error ) {
264     warn "Error creating e911 charge for customer $custnum: $error\n";
265     $num_errors++;
266   } else {
267     $num_e911++;
268     $num_lines += $quantity;
269   }
270 }
271
272 $dbh->commit;
273
274 mylog('debug', "
275 Finished!
276   Processed files: @$files
277   Created charges: $num_charges
278   Sum of charges: \$".sprintf('%0.2f', $sum_charges)."
279   E911 charges: $num_e911
280   E911 lines: $num_lines
281   Errors: $num_errors
282 ");
283
284 sub sftp_connect {
285   my ($host, $sftpuser, $port) = @_;
286   my $sftp;
287   my $connection_tries = 1;
288
289   while (1) {
290       mylog('info', "Connecting to $sftpuser\@$host try number $connection_tries...");
291       $sftp = Net::SFTP::Foreign->new(
292         host      => $host,
293         user      => $sftpuser,
294         port      => $port,
295         # for now we don't support passwords. use authorized_keys.
296         timeout   => 30,
297         #more      => ($opt{v} ? '-v' : ''),
298       );
299
300       if ($sftp->error && $connection_tries < 1200) {
301         $connection_tries++;
302         mylog('error', "Connection failed to $sftpuser\@$host: ". $sftp->error.
303               ", trying again in 60 sec...");
304         sleep 60;
305       }
306       else { last; }
307   }
308
309   return $sftp;
310 }
311
312 our $log;
313 sub mylog {
314   my( $level, $message ) = @_;
315   #warn "$message\n" if $opt{v};
316   print STDERR "$message\n" if $opt{v};
317   $log ||= FS::Log->new('freeside-ipifony-download');
318   $log->log(level=>$level, message=>$message);
319 }
320
321 =head1 NAME
322
323 freeside-ipifony-download - Download and import invoice items from IPifony.
324
325 =head1 SYNOPSIS
326
327       freeside-ipifony-download 
328         [ -v ]
329         [ -q ]
330         [ -N ]
331         [ -a archivedir ]
332         [ -P port ]
333         [ -C category ]
334         [ -T taxclass ]
335         [ -e pkgpart ]
336         freesideuser sftpuser@hostname[:path]
337
338 =head1 REQUIRED PARAMETERS
339
340 I<freesideuser>: the Freeside user to run as.
341
342 I<sftpuser>: the SFTP user to connect as.  The 'freeside' system user should 
343 have an authorization key to connect as that user.
344
345 I<hostname>: the SFTP server.
346
347 I<path>: the path on the server to the working directory. The working
348 directory is the one containing the "ready/" and "done/" subdirectories.
349
350 =head1 OPTIONAL PARAMETERS
351
352 -v: Be verbose; send debugging information to STDERR in addition to the
353 internal log..
354
355 -q: Include the quantity and unit price in the charge description.
356
357 -N: Always bill the charges on the customer's next bill date, if they have
358 one. Otherwise, charges will be billed on the next bill date only if it's
359 within the current calendar month.
360
361 -a I<archivedir>: Save a copy of the downloaded file to I<archivedir>.
362
363 -P I<port>: Connect to that TCP port.
364
365 -C I<category>: The name of a package category to use when creating package
366 classes.
367
368 -e I<pkgpart>: The pkgpart (L<FS::part_pkg>) to use for E911 charges.  A 
369 package of this type will be ordered for each invoice that has E911-subject
370 line items.  The 'quantity' field on this package will be set to the total 
371 quantity of those line items.
372
373 The E911 package must be a one-time package (flat rate, no frequency, no 
374 recurring fee) with setup fee equal to the fee per line.
375
376 =cut
377
378 1;
379