X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=blobdiff_plain;f=FS%2Fbin%2Ffreeside-ipifony-download;h=1e77c3a757c04d3dc26d65aeba7412e203ad9720;hp=ac9f76400f85bb7e1025ec5db2e50c2cbd1c1454;hb=bb7e827141c9ed68f30765c9ca2ddcd1d760ad2d;hpb=cb66e7cac35892a482cf07c7e05db5ff6296c395 diff --git a/FS/bin/freeside-ipifony-download b/FS/bin/freeside-ipifony-download index ac9f76400..1e77c3a75 100644 --- a/FS/bin/freeside-ipifony-download +++ b/FS/bin/freeside-ipifony-download @@ -5,22 +5,39 @@ use Getopt::Std; use Date::Format qw(time2str); use File::Temp qw(tempdir); use Net::SFTP::Foreign; +use File::Copy qw(copy); +use Text::CSV; use FS::UID qw(adminsuidsetup); use FS::Record qw(qsearch qsearchs); use FS::cust_main; use FS::Conf; -use Text::CSV; +use FS::Log; + +our %opt; +getopts('vqNa:P:C:e:', \%opt); -my %opt; -getopts('va:P:', \%opt); +# Product codes that are subject to flat rate E911 charges. For these +# products, the'quantity' field represents the number of lines. +my @E911_CODES = ( 'V-HPBX', 'V-TRUNK' ); + +# Map TAXNONVOICE/TAXVOICE to Freeside taxclass names +my %TAXCLASSES = ( + 'TAXNONVOICE' => 'Other', + 'TAXVOICE' => 'VoIP', +); + #$Net::SFTP::Foreign::debug = -1; sub HELP_MESSAGE { ' Usage: freeside-ipifony-download [ -v ] + [ -q ] + [ -N ] [ -a archivedir ] [ -P port ] + [ -C category ] + [ -e pkgpart ] freesideuser sftpuser@hostname[:path] ' } @@ -28,12 +45,14 @@ my @fields = ( 'custnum', 'date_desc', 'quantity', - 'amount', + 'unit_price', 'classname', + 'taxclass', ); my $user = shift or die &HELP_MESSAGE; -adminsuidsetup $user; +my $dbh = adminsuidsetup $user; +$FS::UID::AutoCommit = 0; # for statistics my $num_charges = 0; @@ -49,6 +68,26 @@ if ( $opt{a} ) { unless -w $opt{a}; } +my $e911_part_pkg; +if ( $opt{e} ) { + $e911_part_pkg = FS::part_pkg->by_key($opt{e}) + or die "E911 pkgpart $opt{e} not found.\n"; + + if ( $e911_part_pkg->base_recur > 0 or $e911_part_pkg->freq ) { + die "E911 pkgpart $opt{e} must be a one-time charge.\n"; + } +} + +my $categorynum = ''; +if ( $opt{C} ) { + # find this category (don't auto-create it, it should exist already) + my $category = qsearchs('pkg_category', { categoryname => $opt{C} }); + if (!defined($category)) { + die "Package category '$opt{C}' does not exist.\n"; + } + $categorynum = $category->categorynum; +} + #my $tmpdir = File::Temp->newdir(); my $tmpdir = tempdir( CLEANUP => 1 ); #DIR=>somewhere? @@ -66,63 +105,59 @@ if ( $opt{P} =~ /^(\d+)$/ ) { } # for now assume SFTP download as the only method -print STDERR "Connecting to $sftpuser\@$host...\n" if $opt{v}; - -my $sftp = Net::SFTP::Foreign->new( - host => $host, - user => $sftpuser, - port => $port, - # for now we don't support passwords. use authorized_keys. - timeout => 30, - more => ($opt{v} ? '-v' : ''), -); -die "failed to connect to '$sftpuser\@$host'\n(".$sftp->error.")\n" - if $sftp->error; +my $sftp = sftp_connect($host, $sftpuser, $port); +if ( $sftp->error ) { + my $error = "Connection failed to $sftpuser\@$host: ". $sftp->error. + ", giving up."; + mylog('critical', $error); + die $error; +} $sftp->setcwd($path) if $path; -my $files = $sftp->ls('.', wanted => qr/\.csv$/, names_only => 1); +my $files = $sftp->ls('ready', wanted => qr/\.csv$/, names_only => 1); if (!@$files) { - print STDERR "No charge files found.\n" if $opt{v}; + mylog('warning',"No charge files found."); exit(-1); } + +my %cust_main; # cache +my %e911_qty; # custnum => sum of E911-subject quantity + +my %is_e911 = map {$_ => 1} @E911_CODES; + FILE: foreach my $filename (@$files) { - print STDERR "Retrieving $filename\n" if $opt{v}; - $sftp->get("$filename", "$tmpdir/$filename"); + mylog('debug', "Retrieving $filename"); + $sftp->get("ready/$filename", "$tmpdir/$filename"); if($sftp->error) { warn "failed to download $filename\n"; next FILE; } # make sure server archive dir exists - if ( !$sftp->stat('Archive') ) { - print STDERR "Creating $path/Archive\n" if $opt{v}; - $sftp->mkdir('Archive'); + if ( !$sftp->stat('done') ) { + mylog('debug',"Creating $path/done"); + $sftp->mkdir('done'); if($sftp->error) { # something is seriously wrong die "failed to create archive directory on server:\n".$sftp->error."\n"; } } #move to server archive dir - $sftp->rename("$filename", "Archive/$filename"); + $sftp->rename("ready/$filename", "done/$filename"); if($sftp->error) { warn "failed to archive $filename on server:\n".$sftp->error."\n"; } # process it anyway, I guess/ #copy to local archive dir if ( $opt{a} ) { - print STDERR "Copying $tmpdir/$filename to archive dir $opt{a}\n" - if $opt{v}; + mylog('debug', "Copying $tmpdir/$filename to archive dir $opt{a}"); copy("$tmpdir/$filename", $opt{a}); + #log too? what's -a all about anyway? warn "failed to copy $tmpdir/$filename to $opt{a}: $!" if $!; } open my $fh, "<$tmpdir/$filename"; - my $header = <$fh>; - if ($header !~ /^cust_id/) { - warn "warning: $filename has incorrect header row:\n$header\n"; - # but try anyway - } my $csv = Text::CSV->new; # orthodox CSV my %hash; while (my $line = <$fh>) { @@ -131,82 +166,212 @@ FILE: foreach my $filename (@$files) { next FILE; }; @hash{@fields} = $csv->fields(); - my $cust_main = FS::cust_main->by_key($hash{custnum}); + if ( $hash{custnum} =~ /^cust/ ) { + # there appears to be a header row + mylog('debug', "skipping header row"); + next; + } + my $cust_main = + $cust_main{$hash{custnum}} ||= FS::cust_main->by_key($hash{custnum}); if (!$cust_main) { warn "customer #$hash{custnum} not found\n"; next; } - print STDERR "Found customer #$hash{custnum}: ".$cust_main->name."\n" - if $opt{v}; + mylog('debug',"Found customer #$hash{custnum}: ".$cust_main->name); + + my $amount = sprintf('%.2f',$hash{quantity} * $hash{unit_price}); + + # bill the charge on the customer's next bill date, if that's within + # the current calendar month; otherwise bill it immediately + # (see RT#24325) + my $next_bill_date = $cust_main->next_bill_date; + if ( $next_bill_date ) { + my ($bill_month, $bill_year) = (localtime($next_bill_date))[4, 5]; + my ($this_month, $this_year) = (localtime(time))[4, 5]; + if ( $opt{N} or + $this_month == $bill_month and $this_year == $bill_year ) { + $cust_main->set('charge_date', $next_bill_date); + } + } # construct arguments for $cust_main->charge - my %opt = ( - amount => $hash{amount}, + my %charge_opt = ( + amount => $hash{unit_price}, quantity => $hash{quantity}, - start_date => $cust_main->next_bill_date, + start_date => $cust_main->get('charge_date'), pkg => $hash{date_desc}, + taxclass => $TAXCLASSES{ $hash{taxclass} }, ); + if ( $opt{q} ) { + $charge_opt{pkg} .= ' (' . $hash{quantity} . ' @ $' . $hash{unit_price} . ' ea)'; + } if (my $classname = $hash{classname}) { if (!exists($classnum_of{$classname}) ) { # then look it up - my $pkg_class = qsearch('pkg_class', { classname => $classname }); - $classnum_of{$classname} = $pkg_class ? $pkg_class->classnum : ''; + my $pkg_class = qsearchs('pkg_class', { + classname => $classname, + categorynum => $categorynum, + }); + if (!defined($pkg_class)) { + # then create it + $pkg_class = FS::pkg_class->new({ + classname => $classname, + categorynum => $categorynum, + }); + my $error = $pkg_class->insert; + die "Error creating package class for product code '$classname':\n". + "$error\n" + if $error; + } + + $classnum_of{$classname} = $pkg_class->classnum; } - $opt{classnum} = $classnum_of{$classname}; + $charge_opt{classnum} = $classnum_of{$classname}; } - # XXX what's the tax status of these charges? - print STDERR " Charging $hash{amount}\n" - if $opt{v}; - my $error = $cust_main->charge(\%opt); + mylog('debug', " Charging $hash{unit_price} * $hash{quantity}"); + my $error = $cust_main->charge(\%charge_opt); if ($error) { warn "Error creating charge: $error" if $error; $num_errors++; } else { $num_charges++; - $sum_charges += $hash{amount}; + $sum_charges += $amount; + } + + if ( $opt{e} and $is_e911{$hash{classname}} ) { + $e911_qty{$hash{custnum}} ||= 0; + $e911_qty{$hash{custnum}} += $hash{quantity}; } } #while $line close $fh; } #FILE -if ($opt{v}) { - print STDERR " +# Order E911 packages +my $num_e911 = 0; +my $num_lines = 0; +foreach my $custnum ( keys (%e911_qty) ) { + my $cust_main = $cust_main{$custnum}; + my $quantity = $e911_qty{$custnum}; + next if $quantity == 0; + my $cust_pkg = FS::cust_pkg->new({ + pkgpart => $opt{e}, + custnum => $custnum, + start_date => $cust_main->get('charge_date'), + quantity => $quantity, + }); + my $error = $cust_main->order_pkg({ cust_pkg => $cust_pkg }); + if ( $error ) { + warn "Error creating e911 charge for customer $custnum: $error\n"; + $num_errors++; + } else { + $num_e911++; + $num_lines += $quantity; + } +} + +$dbh->commit; + +mylog('debug', " Finished! Processed files: @$files Created charges: $num_charges Sum of charges: \$".sprintf('%0.2f', $sum_charges)." + E911 charges: $num_e911 + E911 lines: $num_lines Errors: $num_errors -"; +"); + +sub sftp_connect { + my ($host, $sftpuser, $port) = @_; + my $sftp; + my $connection_tries = 1; + + while (1) { + mylog('info', "Connecting to $sftpuser\@$host try number $connection_tries..."); + $sftp = Net::SFTP::Foreign->new( + host => $host, + user => $sftpuser, + port => $port, + # for now we don't support passwords. use authorized_keys. + timeout => 30, + #more => ($opt{v} ? '-v' : ''), + ); + + if ($sftp->error && $connection_tries < 1200) { + $connection_tries++; + mylog('error', "Connection failed to $sftpuser\@$host: ". $sftp->error. + ", trying again in 60 sec..."); + sleep 60; + } + else { last; } + } + + return $sftp; +} + +our $log; +sub mylog { + my( $level, $message ) = @_; + #warn "$message\n" if $opt{v}; + print STDERR "$message\n" if $opt{v}; + $log ||= FS::Log->new('freeside-ipifony-download'); + $log->log(level=>$level, message=>$message); } =head1 NAME -freeside-eftca-download - Retrieve payment batch responses from EFT Canada. +freeside-ipifony-download - Download and import invoice items from IPifony. =head1 SYNOPSIS - freeside-eftca-download [ -v ] [ -a archivedir ] user + freeside-ipifony-download + [ -v ] + [ -q ] + [ -N ] + [ -a archivedir ] + [ -P port ] + [ -C category ] + [ -T taxclass ] + [ -e pkgpart ] + freesideuser sftpuser@hostname[:path] + +=head1 REQUIRED PARAMETERS + +I: the Freeside user to run as. + +I: the SFTP user to connect as. The 'freeside' system user should +have an authorization key to connect as that user. + +I: the SFTP server. + +I: the path on the server to the working directory. The working +directory is the one containing the "ready/" and "done/" subdirectories. -=head1 DESCRIPTION +=head1 OPTIONAL PARAMETERS -Command line tool to download returned payment reports from the EFT Canada -gateway and void the returned payments. Uses the login and password from -'batchconfig-eft_canada'. +-v: Be verbose; send debugging information to STDERR in addition to the +internal log.. --v: Be verbose. +-q: Include the quantity and unit price in the charge description. --a directory: Archive response files in the provided directory. +-N: Always bill the charges on the customer's next bill date, if they have +one. Otherwise, charges will be billed on the next bill date only if it's +within the current calendar month. -user: freeside username +-a I: Save a copy of the downloaded file to I. -=head1 BUGS +-P I: Connect to that TCP port. -You need to manually SFTP to ftp.eftcanada.com from the freeside account -and accept their key before running this script. +-C I: The name of a package category to use when creating package +classes. -=head1 SEE ALSO +-e I: The pkgpart (L) to use for E911 charges. A +package of this type will be ordered for each invoice that has E911-subject +line items. The 'quantity' field on this package will be set to the total +quantity of those line items. -L +The E911 package must be a one-time package (flat rate, no frequency, no +recurring fee) with setup fee equal to the fee per line. =cut