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