use FS::Record qw(qsearch qsearchs);
use FS::cust_main;
use FS::Conf;
+use File::Copy qw(copy);
use Text::CSV;
my %opt;
-getopts('va:P:C:T:', \%opt);
+getopts('vqa:P:C:e:', \%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 ]
[ -a archivedir ]
[ -P port ]
[ -C category ]
- [ -T taxclass ]
+ [ -e pkgpart ]
freesideuser sftpuser@hostname[:path]
' }
'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;
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)
$categorynum = $category->categorynum;
}
-my $taxclass = $opt{T} || '';
-
#my $tmpdir = File::Temp->newdir();
my $tmpdir = tempdir( CLEANUP => 1 ); #DIR=>somewhere?
port => $port,
# for now we don't support passwords. use authorized_keys.
timeout => 30,
- more => ($opt{v} ? '-v' : ''),
+ #more => ($opt{v} ? '-v' : ''),
);
die "failed to connect to '$sftpuser\@$host'\n(".$sftp->error.")\n"
if $sftp->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};
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");
+ $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') ) {
+ print STDERR "Creating $path/done\n" if $opt{v};
+ $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/
}
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>) {
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
+ print STDERR "skipping header row\n" if $opt{v};
+ 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};
+ 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 ( $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 => $taxclass,
+ 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
$classnum_of{$classname} = $pkg_class->classnum;
}
- $opt{classnum} = $classnum_of{$classname};
+ $charge_opt{classnum} = $classnum_of{$classname};
}
- print STDERR " Charging $hash{amount}\n"
+ print STDERR " Charging $hash{unit_price} * $hash{quantity}\n"
if $opt{v};
- my $error = $cust_main->charge(\%opt);
+ 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
+# 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;
+
if ($opt{v}) {
print STDERR "
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
";
}
=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 ]
+ [ -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.
-=head1 DESCRIPTION
+I<hostname>: the SFTP server.
-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'.
+=head1 OPTIONAL PARAMETERS
-v: Be verbose.
--a directory: Archive response files in the provided directory.
+-q: Include the quantity and unit price in the charge description.
-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