doc
[freeside.git] / FS / FS / cust_main.pm
index cefc764..f2c1c4f 100644 (file)
@@ -4,7 +4,12 @@ use strict;
 use vars qw( @ISA $conf $Debug $import );
 use Safe;
 use Carp;
-use Time::Local;
+BEGIN {
+  eval "use Time::Local;";
+  die "Time::Local version 1.05 required with Perl versions before 5.6"
+    if $] < 5.006 && !defined($Time::Local::VERSION);
+  eval "use Time::Local qw(timelocal timelocal_nocheck);";
+}
 use Date::Format;
 #use Date::Manip;
 use Business::CreditCard;
@@ -38,8 +43,7 @@ $Debug = 0;
 $import = 0;
 
 #ask FS::UID to run this stuff for us later
-#$FS::UID::callback{'FS::cust_main'} = sub { 
-install_callback FS::UID sub { 
+$FS::UID::callback{'FS::cust_main'} = sub { 
   $conf = new FS::Conf;
   #yes, need it for stuff below (prolly should be cached)
 };
@@ -159,7 +163,7 @@ FS::Record.  The following fields are currently supported:
 
 =item ship_fax - phone (optional)
 
-=item payby - I<CARD> (credit card - automatic), I<DCRD> (credit card - on-demand), I<CHEK> (electronic check - automatic), I<DCHK> (electronic check - on-demand), I<LECB> (Phone bill billing), I<BILL> (billing), I<COMP> (free), or I<PREPAY> (special billing type: applies a credit - see L<FS::prepay_credit> and sets billing type to I<BILL>)
+=item payby - `CARD' (credit cards), `CHEK' (electronic check), `LECB' (Phone bill billing), `BILL' (billing), `COMP' (free), or `PREPAY' (special billing type: applies a credit - see L<FS::prepay_credit> and sets billing type to BILL)
 
 =item payinfo - card number, P.O., comp issuer (4-8 lowercase alphanumerics; think username) or prepayment identifier (see L<FS::prepay_credit>)
 
@@ -173,8 +177,6 @@ FS::Record.  The following fields are currently supported:
 
 =item comments - comments (optional)
 
-=item referral_custnum - referring customer number
-
 =back
 
 =head1 METHODS
@@ -277,10 +279,26 @@ sub insert {
   }
 
   # packages
-  $error = $self->order_pkgs($cust_pkgs, \$seconds);
-  if ( $error ) {
-    $dbh->rollback if $oldAutoCommit;
-    return $error;
+  foreach my $cust_pkg ( keys %$cust_pkgs ) {
+    $cust_pkg->custnum( $self->custnum );
+    $error = $cust_pkg->insert;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "inserting cust_pkg (transaction rolled back): $error";
+    }
+    foreach my $svc_something ( @{$cust_pkgs->{$cust_pkg}} ) {
+      $svc_something->pkgnum( $cust_pkg->pkgnum );
+      if ( $seconds && $svc_something->isa('FS::svc_acct') ) {
+        $svc_something->seconds( $svc_something->seconds + $seconds );
+        $seconds = 0;
+      }
+      $error = $svc_something->insert;
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        #return "inserting svc_ (transaction rolled back): $error";
+        return $error;
+      }
+    }
   }
 
   if ( $seconds ) {
@@ -300,77 +318,17 @@ sub insert {
     }
   }
 
-  #false laziness with sub replace
-  my $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
-  $error = $queue->insert($self->getfield('last'), $self->company);
+  $error = $self->queue_fuzzyfiles_update;
   if ( $error ) {
     $dbh->rollback if $oldAutoCommit;
-    return "queueing job (transaction rolled back): $error";
+    return "updating fuzzy search cache: $error";
   }
 
-  if ( defined $self->dbdef_table->column('ship_last') && $self->ship_last ) {
-    $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
-    $error = $queue->insert($self->getfield('last'), $self->company);
-    if ( $error ) {
-      $dbh->rollback if $oldAutoCommit;
-      return "queueing job (transaction rolled back): $error";
-    }
-  }
-  #eslaf
-
   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
   '';
 
 }
 
-=item order_pkgs
-
-document me.  like ->insert(%cust_pkg) on an existing record
-
-=cut
-
-sub order_pkgs {
-  my $self = shift;
-  my $cust_pkgs = shift;
-  my $seconds = shift;
-
-  local $SIG{HUP} = 'IGNORE';
-  local $SIG{INT} = 'IGNORE';
-  local $SIG{QUIT} = 'IGNORE';
-  local $SIG{TERM} = 'IGNORE';
-  local $SIG{TSTP} = 'IGNORE';
-  local $SIG{PIPE} = 'IGNORE';
-
-  my $oldAutoCommit = $FS::UID::AutoCommit;
-  local $FS::UID::AutoCommit = 0;
-  my $dbh = dbh;
-
-  foreach my $cust_pkg ( keys %$cust_pkgs ) {
-    $cust_pkg->custnum( $self->custnum );
-    my $error = $cust_pkg->insert;
-    if ( $error ) {
-      $dbh->rollback if $oldAutoCommit;
-      return "inserting cust_pkg (transaction rolled back): $error";
-    }
-    foreach my $svc_something ( @{$cust_pkgs->{$cust_pkg}} ) {
-      $svc_something->pkgnum( $cust_pkg->pkgnum );
-      if ( $seconds && $$seconds && $svc_something->isa('FS::svc_acct') ) {
-        $svc_something->seconds( $svc_something->seconds + $$seconds );
-        $$seconds = 0;
-      }
-      $error = $svc_something->insert;
-      if ( $error ) {
-        $dbh->rollback if $oldAutoCommit;
-        #return "inserting svc_ (transaction rolled back): $error";
-        return $error;
-      }
-    }
-  }
-
-  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
-  ''; #no error
-}
-
 =item delete NEW_CUSTNUM
 
 This deletes the customer.  If there is an error, returns the error, otherwise
@@ -519,34 +477,47 @@ sub replace {
 
   if ( $self->payby =~ /^(CARD|CHEK|LECB)$/ &&
        grep { $self->get($_) ne $old->get($_) } qw(payinfo paydate payname) ) {
-    # card/check info has changed, want to retry realtime_card invoice events
-    #false laziness w/collect
-    foreach my $cust_bill_event (
-      grep {
-             #$_->part_bill_event->plan eq 'realtime-card'
-             $_->part_bill_event->eventcode =~
-                 /^\$cust_bill\->realtime_(card|ach|lec)\(\);$/
-               && $_->status eq 'done'
-               && $_->statustext
-           }
-        map { $_->cust_bill_event }
-          grep { $_->cust_bill_event }
-            $self->open_cust_bill
-
-    ) {
-      my $error = $cust_bill_event->retry;
-      if ( $error ) {
-        $dbh->rollback if $oldAutoCommit;
-        return "error scheduling invoice events for retry: $error";
-      }
+    # card/check/lec info has changed, want to retry realtime_ invoice events
+    my $error = $self->retry_realtime;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
     }
-    #eslaf
+  }
 
+  $error = $self->queue_fuzzyfiles_update;
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return "updating fuzzy search cache: $error";
   }
 
-  #false laziness with sub insert
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+  '';
+
+}
+
+=item queue_fuzzyfiles_update
+
+Used by insert & replace to update the fuzzy search cache
+
+=cut
+
+sub queue_fuzzyfiles_update {
+  my $self = shift;
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
   my $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
-  $error = $queue->insert($self->getfield('last'), $self->company);
+  my $error = $queue->insert($self->getfield('last'), $self->company);
   if ( $error ) {
     $dbh->rollback if $oldAutoCommit;
     return "queueing job (transaction rolled back): $error";
@@ -554,13 +525,12 @@ sub replace {
 
   if ( defined $self->dbdef_table->column('ship_last') && $self->ship_last ) {
     $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
-    $error = $queue->insert($self->getfield('last'), $self->company);
+    $error = $queue->insert($self->getfield('ship_last'), $self->ship_company);
     if ( $error ) {
       $dbh->rollback if $oldAutoCommit;
       return "queueing job (transaction rolled back): $error";
     }
   }
-  #eslaf
 
   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
   '';
@@ -700,11 +670,11 @@ sub check {
     }
   }
 
-  $self->payby =~ /^(CARD|DCRD|CHEK|DCHK|LECB|BILL|COMP|PREPAY)$/
+  $self->payby =~ /^(CARD|CHEK|LECB|BILL|COMP|PREPAY)$/
     or return "Illegal payby: ". $self->payby;
   $self->payby($1);
 
-  if ( $self->payby eq 'CARD' || $self->payby eq 'DCRD' ) {
+  if ( $self->payby eq 'CARD' ) {
 
     my $payinfo = $self->payinfo;
     $payinfo =~ s/\D//g;
@@ -717,7 +687,7 @@ sub check {
     return gettext('unknown_card_type')
       if cardtype($self->payinfo) eq "Unknown";
 
-  } elsif ( $self->payby eq 'CHEK' || $self->payby eq 'DCHK' ) {
+  } elsif ( $self->payby eq 'CHEK' ) {
 
     my $payinfo = $self->payinfo;
     $payinfo =~ s/[^\d\@]//g;
@@ -770,9 +740,7 @@ sub check {
   }
 
   if ( $self->payname eq '' && $self->payby ne 'CHEK' &&
-       ( ! $conf->exists('require_cardname')
-         || $self->payby !~ /^(CARD|DCRD)$/  ) 
-  ) {
+       ( ! $conf->exists('require_cardname') || $self->payby ne 'CARD' ) ) {
     $self->payname( $self->first. " ". $self->getfield('last') );
   } else {
     $self->payname =~ /^([\w \,\.\-\']+)$/
@@ -958,12 +926,10 @@ sub bill {
   my( $total_setup, $total_recur ) = ( 0, 0 );
   #my( $taxable_setup, $taxable_recur ) = ( 0, 0 );
   my @cust_bill_pkg = ();
-  #my $tax = 0;##
+  my $tax = 0;##
   #my $taxable_charged = 0;##
   #my $charged = 0;##
 
-  my %tax;
-
   foreach my $cust_pkg (
     qsearch('cust_pkg', { 'custnum' => $self->custnum } )
   ) {
@@ -982,8 +948,6 @@ sub bill {
     my %hash = $cust_pkg->hash;
     my $old_cust_pkg = new FS::cust_pkg \%hash;
 
-    my @details = ();
-
     # bill setup
     my $setup = 0;
     unless ( $cust_pkg->setup ) {
@@ -994,6 +958,7 @@ sub bill {
                ": $setup_prog";
       };
       $setup_prog = $1;
+      $setup_prog = '0' if $setup_prog =~ /^\s*$/;
 
         #my $cpt = new Safe;
         ##$cpt->permit(); #what is necessary?
@@ -1023,6 +988,7 @@ sub bill {
                ": $recur_prog";
       };
       $recur_prog = $1;
+      $recur_prog = '0' if $recur_prog =~ /^\s*$/;
 
       # shared with $recur_prog
       $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
@@ -1052,7 +1018,7 @@ sub bill {
       $mon += $part_pkg->freq;
       until ( $mon < 12 ) { $mon -= 12; $year++; }
       $cust_pkg->setfield('bill',
-        timelocal($sec,$min,$hour,$mday,$mon,$year));
+        timelocal_nocheck($sec,$min,$hour,$mday,$mon,$year));
       $cust_pkg_mod_flag = 1; 
     }
 
@@ -1079,12 +1045,11 @@ sub bill {
       }
       if ( $setup > 0 || $recur > 0 ) {
         my $cust_bill_pkg = new FS::cust_bill_pkg ({
-          'pkgnum'  => $cust_pkg->pkgnum,
-          'setup'   => $setup,
-          'recur'   => $recur,
-          'sdate'   => $sdate,
-          'edate'   => $cust_pkg->bill,
-          'details' => \@details,
+          'pkgnum' => $cust_pkg->pkgnum,
+          'setup'  => $setup,
+          'recur'  => $recur,
+          'sdate'  => $sdate,
+          'edate'  => $cust_pkg->bill,
         });
         push @cust_bill_pkg, $cust_bill_pkg;
         $total_setup += $setup;
@@ -1161,10 +1126,7 @@ sub bill {
           } #if $cust_main_county->exempt_amount
 
           $taxable_charged = sprintf( "%.2f", $taxable_charged);
-
-          #$tax += $taxable_charged * $cust_main_county->tax / 100
-          $tax{ $cust_main_county->taxname || 'Tax' } +=
-            $taxable_charged * $cust_main_county->tax / 100
+          $tax += $taxable_charged * $cust_main_county->tax / 100
 
         } #unless $self->tax =~ /Y/i
           #       || $self->payby eq 'COMP'
@@ -1197,17 +1159,16 @@ sub bill {
 #      $taxable_charged * ( $cust_main_county->getfield('tax') / 100 )
 #    );
 
-  foreach my $taxname ( grep { $tax{$_} > 0 } keys %tax ) {
-    my $tax = sprintf("%.2f", $tax{$taxname} );
+  $tax = sprintf("%.2f", $tax);
+  if ( $tax > 0 ) {
     $charged = sprintf( "%.2f", $charged+$tax );
 
     my $cust_bill_pkg = new FS::cust_bill_pkg ({
-      'pkgnum'   => 0,
-      'setup'    => $tax,
-      'recur'    => 0,
-      'sdate'    => '',
-      'edate'    => '',
-      'itemdesc' => $taxname,
+      'pkgnum' => 0,
+      'setup'  => $tax,
+      'recur'  => 0,
+      'sdate'  => '',
+      'edate'  => '',
     });
     push @cust_bill_pkg, $cust_bill_pkg;
   }
@@ -1246,9 +1207,8 @@ sub bill {
 (Attempt to) collect money for this customer's outstanding invoices (see
 L<FS::cust_bill>).  Usually used after the bill method.
 
-Depending on the value of `payby', this may print or email an invoice (I<BILL>,
-I<DCRD>, or I<DCHK>), charge a credit card (I<CARD>), charge via electronic
-check/ACH (I<CHEK>), or just add any necessary (pseudo-)payment (I<COMP>).
+Depending on the value of `payby', this may print an invoice (`BILL'), charge
+a credit card (`CARD'), or just add any necessary (pseudo-)payment (`COMP').
 
 Most actions are now triggered by invoice events; see L<FS::part_bill_event>
 and the invoice events web interface.
@@ -1263,7 +1223,10 @@ invoice_time - Use this time when deciding when to print invoices and
 late notices on those invoices.  The default is now.  It is specified as a UNIX timestamp; see L<perlfunc/"time">).  Also see L<Time::Local> and L<Date::Parse>
 for conversion functions.
 
-retry_card - Retry cards even when not scheduled by invoice events.
+retry - Retry card/echeck/LEC transactions even when not scheduled by invoice
+events.
+
+retry_card - Deprecated alias for 'retry'
 
 batch_card - This option is deprecated.  See the invoice events web interface
 to control whether cards are batched or run against a realtime gateway.
@@ -1297,26 +1260,16 @@ sub collect {
     return '';
   }
 
-  if ( exists($options{'retry_card'}) && $options{'retry_card'} ) {
-    #false laziness w/replace
-    foreach my $cust_bill_event (
-      grep {
-             #$_->part_bill_event->plan eq 'realtime-card'
-             $_->part_bill_event->eventcode eq '$cust_bill->realtime_card();'
-               && $_->status eq 'done'
-               && $_->statustext
-           }
-        map { $_->cust_bill_event }
-          grep { $_->cust_bill_event }
-            $self->open_cust_bill
-    ) {
-      my $error = $cust_bill_event->retry;
-      if ( $error ) {
-        $dbh->rollback if $oldAutoCommit;
-        return "error scheduling invoice events for retry: $error";
-      }
+  if ( exists($options{'retry_card'}) ) {
+    carp 'retry_card option passed to collect is deprecated; use retry';
+    $options{'retry'} ||= $options{'retry_card'};
+  }
+  if ( exists($options{'retry'}) && $options{'retry'} ) {
+    my $error = $self->retry_realtime;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
     }
-    #eslaf
   }
 
   foreach my $cust_bill ( $self->cust_bill ) {
@@ -1406,6 +1359,60 @@ sub collect {
 
 }
 
+=item retry_realtime
+
+Schedules realtime credit card / electronic check / LEC billing events for
+for retry.  Useful if card information has changed or manual retry is desired.
+The 'collect' method must be called to actually retry the transaction.
+
+Implementation details: For each of this customer's open invoices, changes
+the status of the first "done" (with statustext error) realtime processing
+event to "failed".
+
+=cut
+
+sub retry_realtime {
+  my $self = shift;
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  foreach my $cust_bill (
+    grep { $_->cust_bill_event }
+      $self->open_cust_bill
+  ) {
+    my @cust_bill_event =
+      sort { $a->part_bill_event->seconds <=> $b->part_bill_event->seconds }
+        grep {
+               #$_->part_bill_event->plan eq 'realtime-card'
+               $_->part_bill_event->eventcode =~
+                   /\$cust_bill\->realtime_(card|ach|lec)/
+                 && $_->status eq 'done'
+                 && $_->statustext
+             }
+          $cust_bill->cust_bill_event;
+    next unless @cust_bill_event;
+    my $error = $cust_bill_event[0]->retry;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "error scheduling invoice event for retry: $error";
+    }
+
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+  '';
+
+}
+
 =item total_owed
 
 Returns the total owed for this customer on all invoices
@@ -2264,4 +2271,3 @@ L<FS::cust_main_invoice>, L<FS::UID>, schema.html from the base documentation.
 
 1;
 
-