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