have the UI use full country names, and state names outside the US...
[freeside.git] / FS / FS / cust_main.pm
index a265e41..a2eb724 100644 (file)
@@ -16,6 +16,7 @@ BEGIN {
 }
 use Digest::MD5 qw(md5_base64);
 use Date::Format;
+use Date::Parse;
 #use Date::Manip;
 use String::Approx qw(amatch);
 use Business::CreditCard 0.28;
@@ -43,6 +44,7 @@ use FS::part_pkg;
 use FS::part_bill_event;
 use FS::cust_bill_event;
 use FS::cust_tax_exempt;
+use FS::cust_tax_exempt_pkg;
 use FS::type_pkgs;
 use FS::payment_gateway;
 use FS::agent_payment_gateway;
@@ -117,8 +119,6 @@ FS::cust_main - Object methods for cust_main records
   $error = $record->collect;
   $error = $record->collect %options;
   $error = $record->collect 'invoice_time'   => $time,
-                            'batch_card'     => 'yes',
-                            'report_badcard' => 'yes',
                           ;
 
 =head1 DESCRIPTION
@@ -284,6 +284,8 @@ sub paymask {
 
 =item referral_custnum - referring customer number
 
+=item spool_cdr - Enable individual CDR spooling, empty or `Y'
+
 =back
 
 =head1 METHODS
@@ -334,7 +336,7 @@ Currently available options are: I<depend_jobnum> and I<noexport>.
 If I<depend_jobnum> is set, all provisioning jobs will have a dependancy
 on the supplied jobnum (they will not run until the specific job completes).
 This can be used to defer provisioning until some action completes (such
-as running the customer's credit card sucessfully).
+as running the customer's credit card successfully).
 
 The I<noexport> option is deprecated.  If I<noexport> is set true, no
 provisioning jobs (exports) are scheduled.  (You can schedule them later with
@@ -478,7 +480,7 @@ Currently available options are: I<depend_jobnum> and I<noexport>.
 If I<depend_jobnum> is set, all provisioning jobs will have a dependancy
 on the supplied jobnum (they will not run until the specific job completes).
 This can be used to defer provisioning until some action completes (such
-as running the customer's credit card sucessfully).
+as running the customer's credit card successfully).
 
 The I<noexport> option is deprecated.  If I<noexport> is set true, no
 provisioning jobs (exports) are scheduled.  (You can schedule them later with
@@ -1257,7 +1259,11 @@ sub check {
 
     my $payinfo = $self->payinfo;
     $payinfo =~ s/[^\d\@]//g;
-    $payinfo =~ /^(\d+)\@(\d{9})$/ or return 'invalid echeck account@aba';
+    if ( $conf->exists('echeck-nonus') ) {
+      $payinfo =~ /^(\d+)\@(\d+)$/ or return 'invalid echeck account@aba';
+    } else {
+      $payinfo =~ /^(\d+)\@(\d{9})$/ or return 'invalid echeck account@aba';
+    }
     $payinfo = "$1\@$2";
     $self->payinfo($payinfo);
     $self->paycvv('') if $self->dbdef_table->column('paycvv');
@@ -1305,7 +1311,7 @@ sub check {
   }
 
   if ( $self->paydate eq '' || $self->paydate eq '-' ) {
-    return "Expriation date required"
+    return "Expiration date required"
       unless $self->payby =~ /^(BILL|PREPAY|CHEK|DCHK|LECB|CASH|WEST|MCRD)$/;
     $self->paydate('');
   } else {
@@ -1336,8 +1342,10 @@ sub check {
     $self->payname($1);
   }
 
-  $self->tax =~ /^(Y?)$/ or return "Illegal tax: ". $self->tax;
-  $self->tax($1);
+  foreach my $flag (qw( tax spool_cdr )) {
+    $self->$flag() =~ /^(Y?)$/ or return "Illegal $flag: ". $self->$flag();
+    $self->$flag($1);
+  }
 
   $self->otaker(getotaker) unless $self->otaker;
 
@@ -1617,17 +1625,30 @@ sub bill {
 
   $self->select_for_update; #mutex
 
+  #create a new invoice
+  #(we'll remove it later if it doesn't actually need to be generated [contains
+  # no line items] and we're inside a transaciton so nothing else will see it)
+  my $cust_bill = new FS::cust_bill ( {
+    'custnum' => $self->custnum,
+    '_date'   => $time,
+    #'charged' => $charged,
+    'charged' => 0,
+  } );
+  $error = $cust_bill->insert;
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return "can't create invoice for customer #". $self->custnum. ": $error";
+  }
+  my $invnum = $cust_bill->invnum;
+
+  ###
   # find the packages which are due for billing, find out how much they are
   # & generate invoice database.
-  my( $total_setup, $total_recur ) = ( 0, 0 );
-  #my( $taxable_setup, $taxable_recur ) = ( 0, 0 );
-  my @cust_bill_pkg = ();
-  #my $tax = 0;##
-  #my $taxable_charged = 0;##
-  #my $charged = 0;##
+  ###
 
+  my( $total_setup, $total_recur ) = ( 0, 0 );
   my %tax;
+  my @precommit_hooks = ();
 
   foreach my $cust_pkg (
     qsearch('cust_pkg', { 'custnum' => $self->custnum } )
@@ -1649,7 +1670,10 @@ sub bill {
 
     my @details = ();
 
+    ###
     # bill setup
+    ###
+
     my $setup = 0;
     if ( !$cust_pkg->setup || $options{'resetup'} ) {
     
@@ -1658,13 +1682,16 @@ sub bill {
       $setup = eval { $cust_pkg->calc_setup( $time ) };
       if ( $@ ) {
         $dbh->rollback if $oldAutoCommit;
-        return $@;
+        return "$@ running calc_setup for $cust_pkg\n";
       }
 
       $cust_pkg->setfield('setup', $time) unless $cust_pkg->setup;
     }
 
-    #bill recurring fee
+    ###
+    # bill recurring fee
+    ### 
+
     my $recur = 0;
     my $sdate;
     if ( $part_pkg->getfield('freq') ne '0' &&
@@ -1677,10 +1704,13 @@ sub bill {
       # XXX shared with $recur_prog
       $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
 
-      $recur = eval { $cust_pkg->calc_recur( \$sdate, \@details ) };
+      #over two params!  lets at least switch to a hashref for the rest...
+      my %param = ( 'precommit_hooks' => \@precommit_hooks, );
+
+      $recur = eval { $cust_pkg->calc_recur( \$sdate, \@details, \%param ) };
       if ( $@ ) {
         $dbh->rollback if $oldAutoCommit;
-        return $@;
+        return "$@ running calc_recur for $cust_pkg\n";
       }
 
       #change this bit to use Date::Manip? CAREFUL with timezones (see
@@ -1719,6 +1749,10 @@ sub bill {
     warn "\$recur is undefined" unless defined($recur);
     warn "\$cust_pkg->bill is undefined" unless defined($cust_pkg->bill);
 
+    ###
+    # If $cust_pkg has been modified, update it and create cust_bill_pkg records
+    ###
+
     if ( $cust_pkg->modified ) {
 
       warn "  package ". $cust_pkg->pkgnum. " modified; updating\n"
@@ -1740,10 +1774,13 @@ sub bill {
         $dbh->rollback if $oldAutoCommit;
         return "negative recur $recur for pkgnum ". $cust_pkg->pkgnum;
       }
+
       if ( $setup != 0 || $recur != 0 ) {
-        warn "    charges (setup=$setup, recur=$recur); queueing line items\n"
+
+        warn "    charges (setup=$setup, recur=$recur); adding line items\n"
           if $DEBUG > 1;
         my $cust_bill_pkg = new FS::cust_bill_pkg ({
+          'invnum'  => $invnum,
           'pkgnum'  => $cust_pkg->pkgnum,
           'setup'   => $setup,
           'recur'   => $recur,
@@ -1751,10 +1788,18 @@ sub bill {
           'edate'   => $cust_pkg->bill,
           'details' => \@details,
         });
-        push @cust_bill_pkg, $cust_bill_pkg;
+        $error = $cust_bill_pkg->insert;
+        if ( $error ) {
+          $dbh->rollback if $oldAutoCommit;
+          return "can't create invoice line item for invoice #$invnum: $error";
+        }
         $total_setup += $setup;
         $total_recur += $recur;
 
+        ###
+        # handle taxes
+        ###
+
         unless ( $self->tax =~ /Y/i || $self->payby eq 'COMP' ) {
 
           my @taxes = qsearch( 'cust_main_county', {
@@ -1803,7 +1848,8 @@ sub bill {
             next unless $taxable_charged;
 
             if ( $tax->exempt_amount && $tax->exempt_amount > 0 ) {
-              my ($mon,$year) = (localtime($sdate) )[4,5];
+              #my ($mon,$year) = (localtime($sdate) )[4,5];
+              my ($mon,$year) = (localtime( $sdate || $cust_bill->_date ) )[4,5];
               $mon++;
               my $freq = $part_pkg->freq || 1;
               if ( $freq !~ /(\d+)$/ ) {
@@ -1811,40 +1857,74 @@ sub bill {
                 return "daily/weekly package definitions not (yet?)".
                        " compatible with monthly tax exemptions";
               }
-              my $taxable_per_month = sprintf("%.2f", $taxable_charged / $freq );
+              my $taxable_per_month =
+                sprintf("%.2f", $taxable_charged / $freq );
+
+              #call the whole thing off if this customer has any old
+              #exemption records...
+              my @cust_tax_exempt =
+                qsearch( 'cust_tax_exempt' => { custnum=> $self->custnum } );
+              if ( @cust_tax_exempt ) {
+                $dbh->rollback if $oldAutoCommit;
+                return
+                  'this customer still has old-style tax exemption records; '.
+                  'run bin/fs-migrate-cust_tax_exempt?';
+              }
+
               foreach my $which_month ( 1 .. $freq ) {
-                my %hash = (
-                  'custnum' => $self->custnum,
-                  'taxnum'  => $tax->taxnum,
-                  'year'    => 1900+$year,
-                  'month'   => $mon++,
-                );
-                #until ( $mon < 12 ) { $mon -= 12; $year++; }
-                until ( $mon < 13 ) { $mon -= 12; $year++; }
-                my $cust_tax_exempt =
-                  qsearchs('cust_tax_exempt', \%hash)
-                  || new FS::cust_tax_exempt( { %hash, 'amount' => 0 } );
-                my $remaining_exemption = sprintf("%.2f",
-                  $tax->exempt_amount - $cust_tax_exempt->amount );
+
+                #maintain the new exemption table now
+                my $sql = "
+                  SELECT SUM(amount)
+                    FROM cust_tax_exempt_pkg
+                      LEFT JOIN cust_bill_pkg USING ( billpkgnum )
+                      LEFT JOIN cust_bill     USING ( invnum     )
+                    WHERE custnum = ?
+                      AND taxnum  = ?
+                      AND year    = ?
+                      AND month   = ?
+                ";
+                my $sth = dbh->prepare($sql) or do {
+                  $dbh->rollback if $oldAutoCommit;
+                  return "fatal: can't lookup exising exemption: ". dbh->errstr;
+                };
+                $sth->execute(
+                  $self->custnum,
+                  $tax->taxnum,
+                  1900+$year,
+                  $mon,
+                ) or do {
+                  $dbh->rollback if $oldAutoCommit;
+                  return "fatal: can't lookup exising exemption: ". dbh->errstr;
+                };
+                my $existing_exemption = $sth->fetchrow_arrayref->[0] || 0;
+                
+                my $remaining_exemption =
+                  $tax->exempt_amount - $existing_exemption;
                 if ( $remaining_exemption > 0 ) {
                   my $addl = $remaining_exemption > $taxable_per_month
                     ? $taxable_per_month
                     : $remaining_exemption;
                   $taxable_charged -= $addl;
-                  my $new_cust_tax_exempt = new FS::cust_tax_exempt ( {
-                    $cust_tax_exempt->hash,
-                    'amount' =>
-                      sprintf("%.2f", $cust_tax_exempt->amount + $addl),
+
+                  my $cust_tax_exempt_pkg = new FS::cust_tax_exempt_pkg ( {
+                    'billpkgnum' => $cust_bill_pkg->billpkgnum,
+                    'taxnum'     => $tax->taxnum,
+                    'year'       => 1900+$year,
+                    'month'      => $mon,
+                    'amount'     => sprintf("%.2f", $addl ),
                   } );
-                  $error = $new_cust_tax_exempt->exemptnum
-                    ? $new_cust_tax_exempt->replace($cust_tax_exempt)
-                    : $new_cust_tax_exempt->insert;
+                  $error = $cust_tax_exempt_pkg->insert;
                   if ( $error ) {
                     $dbh->rollback if $oldAutoCommit;
-                    return "fatal: can't update cust_tax_exempt: $error";
+                    return "fatal: can't insert cust_tax_exempt_pkg: $error";
                   }
-  
                 } # if $remaining_exemption > 0
+
+                #++
+                $mon++;
+                #until ( $mon < 12 ) { $mon -= 12; $year++; }
+                until ( $mon < 13 ) { $mon -= 12; $year++; }
   
               } #foreach $which_month
   
@@ -1866,85 +1946,50 @@ sub bill {
 
   } #foreach my $cust_pkg
 
-  my $charged = sprintf( "%.2f", $total_setup + $total_recur );
-#  my $taxable_charged = sprintf( "%.2f", $taxable_setup + $taxable_recur );
-
-  unless ( @cust_bill_pkg ) { #don't create invoices with no line items
+  unless ( $cust_bill->cust_bill_pkg ) {
+    $cust_bill->delete; #don't create an invoice w/o line items
     $dbh->commit or die $dbh->errstr if $oldAutoCommit;
     return '';
-  } 
-
-#  unless ( $self->tax =~ /Y/i
-#           || $self->payby eq 'COMP'
-#           || $taxable_charged == 0 ) {
-#    my $cust_main_county = qsearchs('cust_main_county',{
-#        'state'   => $self->state,
-#        'county'  => $self->county,
-#        'country' => $self->country,
-#    } ) or die "fatal: can't find tax rate for state/county/country ".
-#               $self->state. "/". $self->county. "/". $self->country. "\n";
-#    my $tax = sprintf( "%.2f",
-#      $taxable_charged * ( $cust_main_county->getfield('tax') / 100 )
-#    );
-
-  if ( dbdef->table('cust_bill_pkg')->column('itemdesc') ) { #1.5 schema
-
-    foreach my $taxname ( grep { $tax{$_} > 0 } keys %tax ) {
-      my $tax = sprintf("%.2f", $tax{$taxname} );
-      $charged = sprintf( "%.2f", $charged+$tax );
-  
-      my $cust_bill_pkg = new FS::cust_bill_pkg ({
-        'pkgnum'   => 0,
-        'setup'    => $tax,
-        'recur'    => 0,
-        'sdate'    => '',
-        'edate'    => '',
-        'itemdesc' => $taxname,
-      });
-      push @cust_bill_pkg, $cust_bill_pkg;
-    }
+  }
+
+  my $charged = sprintf( "%.2f", $total_setup + $total_recur );
+
+  foreach my $taxname ( grep { $tax{$_} > 0 } keys %tax ) {
+    my $tax = sprintf("%.2f", $tax{$taxname} );
+    $charged = sprintf( "%.2f", $charged+$tax );
   
-  } else { #1.4 schema
-
-    my $tax = 0;
-    foreach ( values %tax ) { $tax += $_ };
-    $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'  => '',
-      });
-      push @cust_bill_pkg, $cust_bill_pkg;
+    my $cust_bill_pkg = new FS::cust_bill_pkg ({
+      'invnum'   => $invnum,
+      'pkgnum'   => 0,
+      'setup'    => $tax,
+      'recur'    => 0,
+      'sdate'    => '',
+      'edate'    => '',
+      'itemdesc' => $taxname,
+    });
+    $error = $cust_bill_pkg->insert;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "can't create invoice line item for invoice #$invnum: $error";
     }
+    $total_setup += $tax;
 
   }
 
-  my $cust_bill = new FS::cust_bill ( {
-    'custnum' => $self->custnum,
-    '_date'   => $time,
-    'charged' => $charged,
-  } );
-  $error = $cust_bill->insert;
+  $cust_bill->charged( sprintf( "%.2f", $total_setup + $total_recur ) );
+  $error = $cust_bill->replace;
   if ( $error ) {
     $dbh->rollback if $oldAutoCommit;
-    return "can't create invoice for customer #". $self->custnum. ": $error";
+    return "can't update charged for invoice #$invnum: $error";
   }
 
-  my $invnum = $cust_bill->invnum;
-  my $cust_bill_pkg;
-  foreach $cust_bill_pkg ( @cust_bill_pkg ) {
-    #warn $invnum;
-    $cust_bill_pkg->invnum($invnum);
-    $error = $cust_bill_pkg->insert;
-    if ( $error ) {
+  foreach my $hook ( @precommit_hooks ) { 
+    eval {
+      &{$hook}; #($self) ?
+    };
+    if ( $@ ) {
       $dbh->rollback if $oldAutoCommit;
-      return "can't create invoice line item for customer #". $self->custnum.
-             ": $error";
+      return "$@ running precommit hook $hook\n";
     }
   }
   
@@ -1977,17 +2022,11 @@ for conversion functions.
 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.
-
-report_badcard - This option is deprecated.
-
-force_print - This option is deprecated; see the invoice events web interface.
-
 quiet - set true to surpress email card/ACH decline notices.
 
+freq - "1d" for the traditional, daily events (the default), or "1m" for the
+new monthly events
+
 =cut
 
 sub collect {
@@ -2028,6 +2067,13 @@ sub collect {
     }
   }
 
+  my $extra_sql = '';
+  if ( defined $options{'freq'} && $options{'freq'} eq '1m' ) {
+    $extra_sql = " AND freq = '1m' ";
+  } else {
+    $extra_sql = " AND ( freq = '1d' OR freq IS NULL OR freq = '' ) ";
+  }
+
   foreach my $cust_bill ( $self->open_cust_bill ) {
 
     # don't try to charge for the same invoice if it's already in a batch
@@ -2049,8 +2095,12 @@ sub collect {
                                 'status'    => 'done',
                                                                    } )
              }
-          qsearch('part_bill_event', { 'payby'    => $self->payby,
-                                       'disabled' => '',           } )
+          qsearch( {
+            'table'     => 'part_bill_event',
+            'hashref'   => { 'payby'    => $self->payby,
+                             'disabled' => '',           },
+            'extra_sql' => $extra_sql,
+          } )
     ) {
 
       last if $cust_bill->owed <= 0  # don't run subsequent events if owed<=0
@@ -2184,7 +2234,7 @@ if set, will override the value from the customer record.
 I<description> is a free-text field passed to the gateway.  It defaults to
 "Internet services".
 
-If an I<invnum> is specified, this payment (if sucessful) is applied to the
+If an I<invnum> is specified, this payment (if successful) is applied to the
 specified invoice.  If you don't specify an I<invnum> you might want to
 call the B<apply_payments> method.
 
@@ -2438,7 +2488,7 @@ sub realtime_bop {
     $capture->submit();
 
     unless ( $capture->is_success ) {
-      my $e = "Authorization sucessful but capture failed, custnum #".
+      my $e = "Authorization successful but capture failed, custnum #".
               $self->custnum. ': '.  $capture->result_code.
               ": ". $capture->error_message;
       warn $e;
@@ -2618,7 +2668,7 @@ gateway is attempted.
 #I<zip>, I<payinfo> and I<paydate> are also available.  Any of these options,
 #if set, will override the value from the customer record.
 
-#If an I<invnum> is specified, this payment (if sucessful) is applied to the
+#If an I<invnum> is specified, this payment (if successful) is applied to the
 #specified invoice.  If you don't specify an I<invnum> you might want to
 #call the B<apply_payments> method.
 
@@ -3937,8 +3987,6 @@ sub batch_import {
   my $pkgpart = $param->{pkgpart};
   my @fields = @{$param->{fields}};
 
-  eval "use Date::Parse;";
-  die $@ if $@;
   eval "use Text::CSV_XS;";
   die $@ if $@;
 
@@ -4044,8 +4092,6 @@ sub batch_charge {
   my $fh = $param->{filehandle};
   my @fields = @{$param->{fields}};
 
-  eval "use Date::Parse;";
-  die $@ if $@;
   eval "use Text::CSV_XS;";
   die $@ if $@;