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