delete fees, RT#81713
[freeside.git] / FS / bin / freeside-ipifony-download
index ac9f764..1e77c3a 100644 (file)
@@ -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<freesideuser>: the Freeside user to run as.
+
+I<sftpuser>: the SFTP user to connect as.  The 'freeside' system user should 
+have an authorization key to connect as that user.
+
+I<hostname>: the SFTP server.
+
+I<path>: 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<archivedir>: Save a copy of the downloaded file to I<archivedir>.
 
-=head1 BUGS
+-P I<port>: 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<category>: The name of a package category to use when creating package
+classes.
 
-=head1 SEE ALSO
+-e I<pkgpart>: The pkgpart (L<FS::part_pkg>) 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<FS::pay_batch>
+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