stray closing /TABLE in the no-ticket case
[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 print STDERR "Connecting to $sftpuser\@$host...\n" if $opt{v};
108
109 my $sftp = Net::SFTP::Foreign->new(
110   host      => $host,
111   user      => $sftpuser,
112   port      => $port,
113   # for now we don't support passwords. use authorized_keys.
114   timeout   => 30,
115   #more      => ($opt{v} ? '-v' : ''),
116 );
117 die "failed to connect to '$sftpuser\@$host'\n(".$sftp->error.")\n"
118   if $sftp->error;
119
120 $sftp->setcwd($path) if $path;
121
122 my $files = $sftp->ls('ready', wanted => qr/\.csv$/, names_only => 1);
123 if (!@$files) {
124   print STDERR "No charge files found.\n" if $opt{v};
125   exit(-1);
126 }
127
128 my %cust_main; # cache
129 my %e911_qty; # custnum => sum of E911-subject quantity
130
131 my %is_e911 = map {$_ => 1} @E911_CODES;
132
133 FILE: foreach my $filename (@$files) {
134   print STDERR "Retrieving $filename\n" if $opt{v};
135   $sftp->get("ready/$filename", "$tmpdir/$filename");
136   if($sftp->error) {
137     warn "failed to download $filename\n";
138     next FILE;
139   }
140
141   # make sure server archive dir exists
142   if ( !$sftp->stat('done') ) {
143     print STDERR "Creating $path/done\n" if $opt{v};
144     $sftp->mkdir('done');
145     if($sftp->error) {
146       # something is seriously wrong
147       die "failed to create archive directory on server:\n".$sftp->error."\n";
148     }
149   }
150   #move to server archive dir
151   $sftp->rename("ready/$filename", "done/$filename");
152   if($sftp->error) {
153     warn "failed to archive $filename on server:\n".$sftp->error."\n";
154   } # process it anyway, I guess/
155
156   #copy to local archive dir
157   if ( $opt{a} ) {
158     print STDERR "Copying $tmpdir/$filename to archive dir $opt{a}\n"
159       if $opt{v};
160     copy("$tmpdir/$filename", $opt{a});
161     warn "failed to copy $tmpdir/$filename to $opt{a}: $!" if $!;
162   }
163
164   open my $fh, "<$tmpdir/$filename";
165   my $csv = Text::CSV->new; # orthodox CSV
166   my %hash;
167   while (my $line = <$fh>) {
168     $csv->parse($line) or do {
169       warn "can't parse $filename: ".$csv->error_input."\n";
170       next FILE;
171     };
172     @hash{@fields} = $csv->fields();
173     if ( $hash{custnum} =~ /^cust/ ) {
174       # there appears to be a header row
175       print STDERR "skipping header row\n" if $opt{v};
176       next;
177     }
178     my $cust_main = 
179       $cust_main{$hash{custnum}} ||= FS::cust_main->by_key($hash{custnum});
180     if (!$cust_main) {
181       warn "customer #$hash{custnum} not found\n";
182       next;
183     }
184     print STDERR "Found customer #$hash{custnum}: ".$cust_main->name."\n"
185       if $opt{v};
186
187     my $amount = sprintf('%.2f',$hash{quantity} * $hash{unit_price});
188
189     # bill the charge on the customer's next bill date, if that's within
190     # the current calendar month; otherwise bill it immediately
191     # (see RT#24325)
192     my $next_bill_date = $cust_main->next_bill_date;
193     if ( $next_bill_date ) {
194       my ($bill_month, $bill_year) = (localtime($next_bill_date))[4, 5];
195       my ($this_month, $this_year) = (localtime(time))[4, 5];
196       if ( $opt{N} or 
197            $this_month == $bill_month and $this_year == $bill_year ) {
198         $cust_main->set('charge_date', $next_bill_date);
199       }
200     }
201
202     # construct arguments for $cust_main->charge
203     my %charge_opt = (
204       amount      => $hash{unit_price},
205       quantity    => $hash{quantity},
206       start_date  => $cust_main->get('charge_date'),
207       pkg         => $hash{date_desc},
208       taxclass    => $TAXCLASSES{ $hash{taxclass} },
209     );
210     if ( $opt{q} ) {
211       $charge_opt{pkg} .= ' (' . $hash{quantity} . ' @ $' . $hash{unit_price} . ' ea)';
212     }
213     if (my $classname = $hash{classname}) {
214       if (!exists($classnum_of{$classname}) ) {
215         # then look it up
216         my $pkg_class = qsearchs('pkg_class', {
217             classname   => $classname,
218             categorynum => $categorynum,
219         });
220         if (!defined($pkg_class)) {
221           # then create it
222           $pkg_class = FS::pkg_class->new({
223               classname   => $classname,
224               categorynum => $categorynum,
225           });
226           my $error = $pkg_class->insert;
227           die "Error creating package class for product code '$classname':\n".
228             "$error\n"
229             if $error;
230         }
231
232         $classnum_of{$classname} = $pkg_class->classnum;
233       }
234       $charge_opt{classnum} = $classnum_of{$classname};
235     }
236     print STDERR "  Charging $hash{unit_price} * $hash{quantity}\n"
237       if $opt{v};
238     my $error = $cust_main->charge(\%charge_opt);
239     if ($error) {
240       warn "Error creating charge: $error" if $error;
241       $num_errors++;
242     } else {
243       $num_charges++;
244       $sum_charges += $amount;
245     }
246
247     if ( $opt{e} and $is_e911{$hash{classname}} ) {
248       $e911_qty{$hash{custnum}} ||= 0;
249       $e911_qty{$hash{custnum}} += $hash{quantity};
250     }
251   } #while $line
252   close $fh;
253 } #FILE
254
255 # Order E911 packages
256 my $num_e911 = 0;
257 my $num_lines = 0;
258 foreach my $custnum ( keys (%e911_qty) ) {
259   my $cust_main = $cust_main{$custnum};
260   my $quantity = $e911_qty{$custnum};
261   next if $quantity == 0;
262   my $cust_pkg = FS::cust_pkg->new({
263       pkgpart     => $opt{e},
264       custnum     => $custnum,
265       start_date  => $cust_main->get('charge_date'),
266       quantity    => $quantity,
267   });
268   my $error = $cust_main->order_pkg({ cust_pkg => $cust_pkg });
269   if ( $error ) {
270     warn "Error creating e911 charge for customer $custnum: $error\n";
271     $num_errors++;
272   } else {
273     $num_e911++;
274     $num_lines += $quantity;
275   }
276 }
277
278 $dbh->commit;
279
280 if ($opt{v}) {
281   print STDERR "
282 Finished!
283   Processed files: @$files
284   Created charges: $num_charges
285   Sum of charges: \$".sprintf('%0.2f', $sum_charges)."
286   E911 charges: $num_e911
287   E911 lines: $num_lines
288   Errors: $num_errors
289 ";
290 }
291
292 =head1 NAME
293
294 freeside-ipifony-download - Download and import invoice items from IPifony.
295
296 =head1 SYNOPSIS
297
298       freeside-ipifony-download 
299         [ -v ]
300         [ -q ]
301         [ -N ]
302         [ -a archivedir ]
303         [ -P port ]
304         [ -C category ]
305         [ -T taxclass ]
306         [ -e pkgpart ]
307         freesideuser sftpuser@hostname[:path]
308
309 =head1 REQUIRED PARAMETERS
310
311 I<freesideuser>: the Freeside user to run as.
312
313 I<sftpuser>: the SFTP user to connect as.  The 'freeside' system user should 
314 have an authorization key to connect as that user.
315
316 I<hostname>: the SFTP server.
317
318 I<path>: the path on the server to the working directory. The working
319 directory is the one containing the "ready/" and "done/" subdirectories.
320
321 =head1 OPTIONAL PARAMETERS
322
323 -v: Be verbose.
324
325 -q: Include the quantity and unit price in the charge description.
326
327 -N: Always bill the charges on the customer's next bill date, if they have
328 one. Otherwise, charges will be billed on the next bill date only if it's
329 within the current calendar month.
330
331 -a I<archivedir>: Save a copy of the downloaded file to I<archivedir>.
332
333 -P I<port>: Connect to that TCP port.
334
335 -C I<category>: The name of a package category to use when creating package
336 classes.
337
338 -e I<pkgpart>: The pkgpart (L<FS::part_pkg>) to use for E911 charges.  A 
339 package of this type will be ordered for each invoice that has E911-subject
340 line items.  The 'quantity' field on this package will be set to the total 
341 quantity of those line items.
342
343 The E911 package must be a one-time package (flat rate, no frequency, no 
344 recurring fee) with setup fee equal to the fee per line.
345
346 =cut
347
348 1;
349