add cust_main.archived field, skip billing if Y, RT#4412
[freeside.git] / FS / FS / cust_main.pm
index 27a617e..3296cf5 100644 (file)
@@ -76,6 +76,8 @@ $skip_fuzzyfiles = 0;
 $ignore_expired_card = 0;
 
 @encrypted_fields = ('payinfo', 'paycvv');
+sub nohistory_fields { ('paycvv'); }
+
 @paytypes = ('', 'Personal checking', 'Personal savings', 'Business checking', 'Business savings');
 
 #ask FS::UID to run this stuff for us later
@@ -1772,7 +1774,7 @@ sub check {
     $self->payname($1);
   }
 
-  foreach my $flag (qw( tax spool_cdr squelch_cdr )) {
+  foreach my $flag (qw( tax spool_cdr squelch_cdr archived )) {
     $self->$flag() =~ /^(Y?)$/ or return "Illegal $flag: ". $self->$flag();
     $self->$flag($1);
   }
@@ -1809,7 +1811,7 @@ sub has_ship_address {
   scalar( grep { $self->getfield("ship_$_") ne '' } $self->addr_fields );
 }
 
-=item all_pkgs
+=item all_pkgs [ EXTRA_QSEARCH_PARAMS_HASHREF ]
 
 Returns all packages (see L<FS::cust_pkg>) for this customer.
 
@@ -1817,14 +1819,15 @@ Returns all packages (see L<FS::cust_pkg>) for this customer.
 
 sub all_pkgs {
   my $self = shift;
+  my $extra_qsearch = ref($_[0]) ? shift : {};
 
-  return $self->num_pkgs unless wantarray;
+  return $self->num_pkgs unless wantarray || keys(%$extra_qsearch);
 
   my @cust_pkg = ();
   if ( $self->{'_pkgnum'} ) {
     @cust_pkg = values %{ $self->{'_pkgnum'}->cache };
   } else {
-    @cust_pkg = qsearch( 'cust_pkg', { 'custnum' => $self->custnum });
+    @cust_pkg = $self->_cust_pkg($extra_qsearch);
   }
 
   sort sort_packages @cust_pkg;
@@ -1851,7 +1854,7 @@ sub cust_location {
   qsearch('cust_location', { 'custnum' => $self->custnum } );
 }
 
-=item ncancelled_pkgs
+=item ncancelled_pkgs [ EXTRA_QSEARCH_PARAMS_HASHREF ]
 
 Returns all non-cancelled packages (see L<FS::cust_pkg>) for this customer.
 
@@ -1859,6 +1862,7 @@ Returns all non-cancelled packages (see L<FS::cust_pkg>) for this customer.
 
 sub ncancelled_pkgs {
   my $self = shift;
+  my $extra_qsearch = ref($_[0]) ? shift : {};
 
   return $self->num_ncancelled_pkgs unless wantarray;
 
@@ -1877,33 +1881,56 @@ sub ncancelled_pkgs {
          $self->custnum. "\n"
       if $DEBUG > 1;
 
-    @cust_pkg =
-      qsearch( 'cust_pkg', {
-                             'custnum' => $self->custnum,
-                             'cancel'  => '',
-                           });
-    push @cust_pkg,
-      qsearch( 'cust_pkg', {
-                             'custnum' => $self->custnum,
-                             'cancel'  => 0,
-                           });
+    $extra_qsearch->{'extra_sql'} .= ' AND ( cancel IS NULL OR cancel = 0 ) ';
+
+    @cust_pkg = $self->_cust_pkg($extra_qsearch);
+
   }
 
   sort sort_packages @cust_pkg;
 
 }
 
+sub _cust_pkg {
+  my $self = shift;
+  my $extra_qsearch = ref($_[0]) ? shift : {};
+
+  $extra_qsearch->{'select'} ||= '*';
+  $extra_qsearch->{'select'} .=
+   ',( SELECT COUNT(*) FROM cust_svc WHERE cust_pkg.pkgnum = cust_svc.pkgnum )
+     AS _num_cust_svc';
+
+  map {
+        $_->{'_num_cust_svc'} = $_->get('_num_cust_svc');
+        $_;
+      }
+  qsearch({
+    %$extra_qsearch,
+    'table'   => 'cust_pkg',
+    'hashref' => { 'custnum' => $self->custnum },
+  });
+
+}
+
 # This should be generalized to use config options to determine order.
 sub sort_packages {
-  if ( $a->get('cancel') and $b->get('cancel') ) {
-    $a->pkgnum <=> $b->pkgnum;
-  } elsif ( $a->get('cancel') or $b->get('cancel') ) {
+  
+  if ( $a->get('cancel') xor $b->get('cancel') ) {
     return -1 if $b->get('cancel');
     return  1 if $a->get('cancel');
+    #shouldn't get here...
     return 0;
   } else {
-    $a->pkgnum <=> $b->pkgnum;
+    my $a_num_cust_svc = $a->num_cust_svc;
+    my $b_num_cust_svc = $b->num_cust_svc;
+    return 0  if !$a_num_cust_svc && !$b_num_cust_svc;
+    return -1 if  $a_num_cust_svc && !$b_num_cust_svc;
+    return 1  if !$a_num_cust_svc &&  $b_num_cust_svc;
+    my @a_cust_svc = $a->cust_svc;
+    my @b_cust_svc = $b->cust_svc;
+    $a_cust_svc[0]->svc_x->label cmp $b_cust_svc[0]->svc_x->label;
   }
+
 }
 
 =item suspended_pkgs
@@ -2432,6 +2459,7 @@ sub bill {
   foreach my $tax ( keys %taxlisthash ) {
     my $tax_object = shift @{ $taxlisthash{$tax} };
     warn "found ". $tax_object->taxname. " as $tax\n" if $DEBUG > 2;
+    warn " ". join('/', @{ $taxlisthash{$tax} } ). "\n" if $DEBUG > 2;
     my $hashref_or_error =
       $tax_object->taxline( $taxlisthash{$tax},
                             'custnum'      => $self->custnum,
@@ -2508,20 +2536,20 @@ sub bill {
     my $tax_object = shift @{ $totlisthash{$tax} };
     warn "found previously found taxed tax ". $tax_object->taxname. "\n"
       if $DEBUG > 2;
-    my $listref_or_error =
+    my $hashref_or_error =
       $tax_object->taxline( $totlisthash{$tax},
                             'custnum'      => $self->custnum,
                             'invoice_time' => $invoice_time
                           );
-    unless (ref($listref_or_error)) {
+    unless (ref($hashref_or_error)) {
       $dbh->rollback if $oldAutoCommit;
-      return $listref_or_error;
+      return $hashref_or_error;
     }
 
-    warn "adding taxed tax amount ". $listref_or_error->[1].
+    warn "adding taxed tax amount ". $hashref_or_error->{'amount'}.
          " as ". $tax_object->taxname. "\n"
       if $DEBUG;
-    $tax{ $tax } += $listref_or_error->[1];
+    $tax{ $tax } += $hashref_or_error->{'amount'};
   }
   
   #consolidate and create tax line items
@@ -2914,7 +2942,7 @@ sub _handle_taxes {
 
     foreach my $tax ( @taxes ) {
 
-      my $taxname = ref( $tax ). ' taxnum'. $tax->taxnum;
+      my $taxname = ref( $tax ). ' '. $tax->taxnum;
 #      $taxname .= ' pkgnum'. $cust_pkg->pkgnum.
 #                  ' locationnum'. $cust_pkg->locationnum
 #        if $conf->exists('tax-pkg_address') && $cust_pkg->locationnum;
@@ -3475,24 +3503,35 @@ sub realtime_bop {
   return "Banned credit card" if $ban;
 
   ###
-  # select a gateway
+  # set taxclass and trans_is_recur based on invnum if there is one
   ###
 
   my $taxclass = '';
+  my $trans_is_recur = 0;
   if ( $options{'invnum'} ) {
+
     my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{'invnum'} } );
     die "invnum ". $options{'invnum'}. " not found" unless $cust_bill;
-    my @taxclasses =
-      map  { $_->part_pkg->taxclass }
+
+    my @part_pkg =
+      map  { $_->part_pkg }
       grep { $_ }
       map  { $_->cust_pkg }
       $cust_bill->cust_bill_pkg;
-    unless ( grep { $taxclasses[0] ne $_ } @taxclasses ) { #unless there are
-                                                           #different taxclasses
-      $taxclass = $taxclasses[0];
-    }
+
+    my @taxclasses = map $_->taxclass, @part_pkg;
+    $taxclass = $taxclasses[0]
+      unless grep { $taxclasses[0] ne $_ } @taxclasses; #unless there are
+                                                        #different taxclasses
+    $trans_is_recur = 1
+      if grep { $_->freq ne '0' } @part_pkg;
+
   }
 
+  ###
+  # select a gateway
+  ###
+
   #look for an agent gateway override first
   my $cardtype;
   if ( $method eq 'CC' ) {
@@ -3620,16 +3659,15 @@ sub realtime_bop {
                            : $self->payissue;
     $content{issue_number} = $payissue if $payissue;
 
-    $content{recurring_billing} = 'YES'
-      if qsearch('cust_pay', { 'custnum' => $self->custnum,
-                               'payby'   => 'CARD',
-                               'payinfo' => $payinfo,
-                             } )
-      || qsearch('cust_pay', { 'custnum' => $self->custnum,
-                               'payby'   => 'CARD',
-                               'paymask' => $self->mask_payinfo('CARD', $payinfo),
-                             } );
-
+    if ( $self->_bop_recurring_billing( 'payinfo'        => $payinfo,
+                                        'trans_is_recur' => $trans_is_recur,
+                                      )
+       )
+    {
+      $content{recurring_billing} = 'YES';
+      $content{acct_code} = 'rebill'
+        if $conf->exists('credit_card-recurring_billing_acct_code');
+    }
 
   } elsif ( $method eq 'ECHECK' ) {
     ( $content{account_number}, $content{routing_code} ) =
@@ -3688,15 +3726,16 @@ sub realtime_bop {
   #okay, good to go, if we're a duplicate, cust_pay_pending will kick us out
 
   my $cust_pay_pending = new FS::cust_pay_pending {
-    'custnum'    => $self->custnum,
-    #'invnum'     => $options{'invnum'},
-    'paid'       => $amount,
-    '_date'      => '',
-    'payby'      => $method2payby{$method},
-    'payinfo'    => $payinfo,
-    'paydate'    => $paydate,
-    'status'     => 'new',
-    'gatewaynum' => ( $payment_gateway ? $payment_gateway->gatewaynum : '' ),
+    'custnum'           => $self->custnum,
+    #'invnum'            => $options{'invnum'},
+    'paid'              => $amount,
+    '_date'             => '',
+    'payby'             => $method2payby{$method},
+    'payinfo'           => $payinfo,
+    'paydate'           => $paydate,
+    'recurring_billing' => $content{recurring_billing},
+    'status'            => 'new',
+    'gatewaynum'        => ( $payment_gateway ? $payment_gateway->gatewaynum : '' ),
   };
   $cust_pay_pending->payunique( $options{payunique} )
     if defined($options{payunique}) && length($options{payunique});
@@ -3982,6 +4021,34 @@ sub realtime_bop {
 
 }
 
+sub _bop_recurring_billing {
+  my( $self, %opt ) = @_;
+
+  my $method = $conf->config('credit_card-recurring_billing_flag');
+
+  if ( $method eq 'transaction_is_recur' ) {
+
+    return 1 if $opt{'trans_is_recur'};
+
+  } else {
+
+    my %hash = ( 'custnum' => $self->custnum,
+                 'payby'   => 'CARD',
+               );
+
+    return 1 
+      if qsearch('cust_pay', { %hash, 'payinfo' => $opt{'payinfo'} } )
+      || qsearch('cust_pay', { %hash, 'paymask' => $self->mask_payinfo('CARD',
+                                                               $opt{'payinfo'} )
+                             } );
+
+  }
+
+  return 0;
+
+}
+
+
 =item realtime_refund_bop METHOD [ OPTION => VALUE ... ]
 
 Refunds a realtime credit card, ACH (electronic check) or phone bill transaction
@@ -4534,6 +4601,27 @@ sub _new_realtime_bop {
   $self->_bop_defaults(\%options);
 
   ###
+  # set trans_is_recur based on invnum if there is one
+  ###
+
+  my $trans_is_recur = 0;
+  if ( $options{'invnum'} ) {
+
+    my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{'invnum'} } );
+    die "invnum ". $options{'invnum'}. " not found" unless $cust_bill;
+
+    my @part_pkg =
+      map  { $_->part_pkg }
+      grep { $_ }
+      map  { $_->cust_pkg }
+      $cust_bill->cust_bill_pkg;
+
+    $trans_is_recur = 1
+      if grep { $_->freq ne '0' } @part_pkg;
+
+  }
+
+  ###
   # select a gateway
   ###
 
@@ -4609,16 +4697,15 @@ sub _new_realtime_bop {
                            : $self->payissue;
     $content{issue_number} = $payissue if $payissue;
 
-    $content{recurring_billing} = 'YES'
-      if qsearch('cust_pay', { 'custnum' => $self->custnum,
-                               'payby'   => 'CARD',
-                               'payinfo' => $options{payinfo},
-                             } )
-      || qsearch('cust_pay', { 'custnum' => $self->custnum,
-                               'payby'   => 'CARD',
-                               'paymask' => $self->mask_payinfo('CARD', $options{payinfo}),
-                             } );
-
+    if ( $self->_bop_recurring_billing( 'payinfo'        => $options{'payinfo'},
+                                        'trans_is_recur' => $trans_is_recur,
+                                      )
+       )
+    {
+      $content{recurring_billing} = 'YES';
+      $content{acct_code} = 'rebill'
+        if $conf->exists('credit_card-recurring_billing_acct_code');
+    }
 
   } elsif ( $namespace eq 'Business::OnlinePayment' && $options{method} eq 'ECHECK' ){
     ( $content{account_number}, $content{routing_code} ) =
@@ -4680,17 +4767,18 @@ sub _new_realtime_bop {
   #okay, good to go, if we're a duplicate, cust_pay_pending will kick us out
 
   my $cust_pay_pending = new FS::cust_pay_pending {
-    'custnum'    => $self->custnum,
-    #'invnum'     => $options{'invnum'},
-    'paid'       => $options{amount},
-    '_date'      => '',
-    'payby'      => $bop_method2payby{$options{method}},
-    'payinfo'    => $options{payinfo},
-    'paydate'    => $paydate,
-    'status'     => 'new',
-    'gatewaynum' => $payment_gateway->gatewaynum || '',
-    'session_id' => $options{session_id} || '',
-    'jobnum'     => $options{depend_jobnum} || '',
+    'custnum'           => $self->custnum,
+    #'invnum'            => $options{'invnum'},
+    'paid'              => $options{amount},
+    '_date'             => '',
+    'payby'             => $bop_method2payby{$options{method}},
+    'payinfo'           => $options{payinfo},
+    'paydate'           => $paydate,
+    'recurring_billing' => $content{recurring_billing},
+    'status'            => 'new',
+    'gatewaynum'        => $payment_gateway->gatewaynum || '',
+    'session_id'        => $options{session_id} || '',
+    'jobnum'            => $options{depend_jobnum} || '',
   };
   $cust_pay_pending->payunique( $options{payunique} )
     if defined($options{payunique}) && length($options{payunique});
@@ -8404,6 +8492,15 @@ sub queued_bill {
       );
 }
 
+sub _upgrade_data { #class method
+  my ($class, %opts) = @_;
+
+  my $sql = 'UPDATE h_cust_main SET paycvv = NULL WHERE paycvv IS NOT NULL';
+  my $sth = dbh->prepare($sql) or die dbh->errstr;
+  $sth->execute or die $sth->errstr;
+
+}
+
 =back
 
 =head1 BUGS