#!/usr/bin/perl -w

use strict;
use vars qw( $user $cust_main @customers );
use Getopt::Std;
use FS::UID qw(adminsuidsetup);
use FS::Record qw(qsearchs);
use FS::Conf;
use FS::cust_main;
use FS::cust_pay;
use FS::cust_pay_void;
use Business::OnlinePayment; # For retrieving the void list only.
use Time::Local;
use Date::Parse 'str2time';
use Date::Format 'time2str';

my %opt;
getopts("r:f:ca:g:s:e:vnX:", \%opt);

$user = shift or die &usage;
&adminsuidsetup( $user );

# The -g and -a options need to override this.
my $method = $opt{'c'} ? 'ECHECK' : 'CARD';
my $gateway;
if($opt{'g'}) {
  $gateway = FS::payment_gateway->by_key($opt{'g'})
    or die "Payment gateway not found: '".$opt{'g'}."'.";
}
elsif($opt{'a'}) {
  my $agent = FS::agent->by_key($opt{'a'})
    or die "Agent not found: '".$opt{'a'}."'.";
  $gateway = $agent->payment_gateway(method => $method)
    or die "Agent has no payment gateway for method '$method'.";
}

if(defined($opt{'X'}) and !qsearchs('reason', { reasonnum => opt{'X'} })) {
  die "Cancellation reason not found: '".$opt{'X'}."'";
}

my ($processor, $login, $password, $action, @bop_options) =
  FS::cust_main->default_payment_gateway($method);
my $gatewaynum = '';

if($gateway) {
# override the default gateway
  $gatewaynum = $gateway->gatewaynum . '-' if $gateway->gatewaynum;
  $processor = $gateway->gateway_module;
  $login     = $gateway->gateway_username;
  $password  = $gateway->gateway_password;
  $action    = $gateway->gateway_action;
  @bop_options = $gateway->options;
}

my @auths;
if($opt{'f'}) {
# Read the list of authorization numbers from a file.
  my $in;
  open($in, '< '. $opt{'f'}) or die "Unable to open file: '".$opt{'f'}."'.";
  @auths = grep /^\d+$/, <$in>;
  chomp @auths;
}
else {
# Get the list from the processor.  This requires the processor module to 
# support get_returns.
  my $transaction = new Business::OnlinePayment ( $processor, @bop_options );
  if(! $transaction->can('get_returns')) {
    die "'$processor' does not provide an automated void list.";
  }
  my @local = localtime;
# Start and end dates for this can be set via -s and -e.  If they're not,
# end defaults to midnight today and start defaults to one day before end.
  my $end = defined($opt{'e'}) ? 
      str2time($opt{'e'}) : timelocal(0, 0, 0, @local[3,4,5]);
  my $start = defined($opt{'s'}) ?
      str2time($opt{'s'}) : $end - 86400;
  die "Invalid date range: '$start'-'$end'" if not ($start and $end);
  $transaction->content (
    login     => $login,
    password  => $password,
    start     => time2str("%Y-%m-%d",$start),
    end       => time2str("%Y-%m-%d",$end),
    );
  @auths = $transaction->get_returns;
}

$opt{'r'} ||= 'freeside-void-payments';
my $success = 0;
my $notfound = 0;
my $canceled = 0;
print "Voiding ".scalar(@auths)." transactions:\n" if $opt{'v'};
foreach my $authnum (@auths) {
  my $paybatch = $gatewaynum . $processor . ':' . $authnum;
  my $cust_pay = qsearchs('cust_pay', { paybatch => $paybatch } );
  my $error;
  my $cancel_error;
  if($cust_pay) {
    $error = $cust_pay->void($opt{'r'});
    $success++ if not $error;
    if($opt{'X'} and not $error) {
      $cancel_error = join(';',$cust_pay->cust_main->cancel('reason' => $opt{'X'}));
      $canceled++ if !$cancel_error;
    }
  }
  else {
    my $cpv = qsearchs('cust_pay_void', { paybatch => $paybatch });
    if($cpv) {
      $error = 'already voided '.time2str('%Y-%m-%d', $cpv->void_date) . 
        ' by ' . $cpv->otaker;
    }
    else {
      $error = 'not found';
      $notfound++;
    }
  }
  if($opt{'v'}) {
    print $authnum;
    if($error) {
      print "\t($error)";
    }
    elsif($opt{'X'}) {
      print "\t(canceled service)" if !$cancel_error;
      print "\n\t(cancellation failed: $cancel_error)" if $cancel_error;
    }
    print "\n";
  }
}

if($opt{'v'}) {
  print scalar(@auths)." transactions: $success voided, $notfound not found\n";
  print "$canceled customer".($canceled == 1 ? '' : 's')." canceled\n" if $opt{'X'};
}

sub usage {
    die "Usage:\n\n  freeside-void-payments [ options ] user
    
    options:
      -a agentnum    use agentnum's gateway information
      -g gatewaynum  use gatewaynum
      -f file        read transaction numbers from file
      -c             use ECHECK gateway instead of CARD
      -r reason      specify void reason (as a string)
      -v             be verbose
      -s start-date
      -e end-date    limit by payment return date
      -X reasonnum   cancel customers whose payments are voided
                     (specify cancellation reason number)

";
}

__END__

# Documentation

=head1 NAME

freeside-void-payments - Automatically void a list of returned payments.

=head1 SYNOPSIS

  freeside-void-payments [ -f file | [ -s start-date ] [ -e end-date ] ] 
                         [ -r 'reason' ] 
                         [ -g gatewaynum | -a agentnum ] 
                         [ -c ] [ -v ] 
                         [ -X reasonnum ] 
                         user

=head1 DESCRIPTION

=pod

Voids payments that were returned by the payment processor.  Can be 
run periodically from crontab or manually after receiving a list of 
returned payments.  Normally this is a meaningful operation only for 
electronic checks.

This script voids payments based on the combination of gateway (see 
L<FS::payment_gateway>) and authorization number, since this is 
generally how the processor will identify them later.

  -f: Read the list of authorization numbers from the specified file.  
      If they are not from the default payment gateway, -g or -a 
      must be given to identify the gateway.

  If -f is not given, the script will attempt to contact the gateway 
  and download a list of returned transactions.  To support this, 
  the Business::OnlinePayment module for the processor must implement 
  the get_returns() method.  For an example, see 
  Business::OnlinePayment::WesternACH.

  -s, -e: Specify the starting and ending dates for the void list.  
      This has no effect if -f is given.  The end date defaults to 
      today, and the start date defaults to one day before the end date.

  -r: The reason for voiding the payments, to be stored in the database.

  -g: The FS::payment_gateway number for the gateway that handled 
      these payments.  If -f is not given, this determines which 
      gateway will be contacted.  This overrides -a.

  -a: The agentnum whose default gateway will be used.  If neither -a 
      nor -g is given, the system default gateway will be used.

  -c: Use the default gateway for check transactions rather than 
      credit cards.

  -v: Be verbose.
  
  -X: Automatically cancel all packages belonging to customers whose 
      payments were returned.  Requires a cancellation reasonnum 
      (from FS::reason).

=head1 EXAMPLE

Given 'returns.txt', which contains one authorization number on each 
line, provided by your default e-check processor:

  freeside-void-payments -f returns.txt -c -r 'Returned check'

If your default processor is Western ACH, which supports automated 
returns processing, this voids all returned payments since 2009-06-01:

  freeside-void-payments -r 'Returned check' -s 2009-06-01

This, in your crontab, will void returned payments for the last 
day at 8:30 every morning:

  30 8 * * * /usr/local/bin/freeside-void-payments -r 'Returned check'

=head1 BUGS

Most payment gateways don't support it.

=head1 SEE ALSO

L<Business::OnlinePayment>, L<FS::cust_pay>

=cut
