Revert "Format email addresses w/Email::Address instead of ad-hoc, fixes issues email...
[freeside.git] / FS / FS / cust_main.pm
index b02f7b7..7e1a25d 100644 (file)
@@ -75,6 +75,7 @@ use FS::cust_attachment;
 use FS::contact;
 use FS::Locales;
 use FS::upgrade_journal;
+use FS::reason;
 
 # 1 is mostly method/subroutine entry and options
 # 2 traces progress of some operations
@@ -238,6 +239,10 @@ Name on card or billing name
 
 IP address from which payment information was received
 
+=item paycardtype
+
+The credit card type (deduced from the card number).
+
 =item tax
 
 Tax exempt, empty or `Y'
@@ -1848,6 +1853,7 @@ sub check {
     || $self->ut_floatn('credit_limit')
     || $self->ut_numbern('billday')
     || $self->ut_numbern('prorate_day')
+    || $self->ut_flag('force_prorate_day')
     || $self->ut_flag('edit_subject')
     || $self->ut_flag('calling_list_exempt')
     || $self->ut_flag('invoice_noemail')
@@ -1961,9 +1967,12 @@ sub check {
     validate($payinfo)
       or return gettext('invalid_card'); # . ": ". $self->payinfo;
 
-    return gettext('unknown_card_type')
-      if $self->payinfo !~ /^99\d{14}$/ #token
-      && cardtype($self->payinfo) eq "Unknown";
+    my $cardtype = cardtype($payinfo);
+    $cardtype = 'Tokenized' if $self->payinfo =~ /^99\d{14}$/; # token
+
+    return gettext('unknown_card_type') if $cardtype eq 'Unknown';
+
+    $self->set('paycardtype', $cardtype);
 
     unless ( $ignore_banned_card ) {
       my $ban = FS::banned_pay->ban_search( %{ $self->_banned_pay_hashref } );
@@ -1985,7 +1994,7 @@ sub check {
     }
 
     if (length($self->paycvv) && !$self->is_encrypted($self->paycvv)) {
-      if ( cardtype($self->payinfo) eq 'American Express card' ) {
+      if ( $cardtype eq 'American Express card' ) {
         $self->paycvv =~ /^(\d{4})$/
           or return "CVV2 (CID) for American Express cards is four digits.";
         $self->paycvv($1);
@@ -1998,7 +2007,6 @@ sub check {
       $self->paycvv('');
     }
 
-    my $cardtype = cardtype($payinfo);
     if ( $cardtype =~ /^(Switch|Solo)$/i ) {
 
       return "Start date or issue number is required for $cardtype cards"
@@ -2095,6 +2103,11 @@ sub check {
       unless qsearchs('prepay_credit', { 'identifier' => $self->payinfo } );
     $self->paycvv('');
 
+  } elsif ( $self->payby =~ /^CARD|DCRD$/ and $self->paymask ) {
+    # either ignoring invalid cards, or we can't decrypt the payinfo, but
+    # try to detect the card type anyway. this never returns failure, so
+    # the contract of $ignore_invalid_cards is maintained.
+    $self->set('paycardtype', cardtype($self->paymask));
   }
 
   if ( $self->paydate eq '' || $self->paydate eq '-' ) {
@@ -2146,6 +2159,10 @@ sub check {
     && ! $self->custnum
     && $conf->exists('cust_main-require_locale');
 
+  return "Please select a customer class"
+    if ! $self->classnum
+    && $conf->exists('cust_main-require_classnum');
+
   foreach my $flag (qw( tax spool_cdr squelch_cdr archived email_csv_cdr )) {
     $self->$flag() =~ /^(Y?)$/ or return "Illegal $flag: ". $self->$flag();
     $self->$flag($1);
@@ -2167,10 +2184,14 @@ sub check_payinfo_cardtype {
   my $payinfo = $self->payinfo;
   $payinfo =~ s/\D//g;
 
-  return '' if $payinfo =~ /^99\d{14}$/; #token
+  if ( $payinfo =~ /^99\d{14}$/ ) {
+    $self->set('paycardtype', 'Tokenized');
+    return '';
+  }
 
   my %bop_card_types = map { $_=>1 } values %{ card_types() };
   my $cardtype = cardtype($payinfo);
+  $self->set('paycardtype', $cardtype);
 
   return "$cardtype not accepted" unless $bop_card_types{$cardtype};
 
@@ -2396,7 +2417,11 @@ FS::cust_pkg::cancel() methods.
 
 =item quiet - can be set true to supress email cancellation notices.
 
-=item reason - can be set to a cancellation reason (see L<FS:reason>), either a reasonnum of an existing reason, or passing a hashref will create a new reason.  The hashref should have the following keys: typenum - Reason type (see L<FS::reason_type>, reason - Text of the new reason.
+=item reason - can be set to a cancellation reason (see L<FS:reason>), either a
+reasonnum of an existing reason, or passing a hashref will create a new reason.
+The hashref should have the following keys:
+typenum - Reason type (see L<FS::reason_type>)
+reason - Text of the new reason.
 
 =item cust_pkg_reason - can be an arrayref of L<FS::cust_pkg_reason> objects
 for the individual packages, parallel to the C<cust_pkg> argument. The
@@ -2411,7 +2436,11 @@ reason and reason_otaker arguments will be taken from those objects.
 sub cancel_pkgs {
   my( $self, %opt ) = @_;
 
-  my $oldAutoCommit = $FS::UID::AutoCommit;
+  # we're going to cancel services, which is not reversible
+  # but on 3.x, don't strictly enforce this
+  warn "cancel_pkgs should not be run inside a transaction"
+    if $FS::UID::AutoCommit == 0;
+
   local $FS::UID::AutoCommit = 0;
 
   return ( 'access denied' )
@@ -2427,7 +2456,7 @@ sub cancel_pkgs {
     my $ban = new FS::banned_pay $self->_new_banned_pay_hashref;
     my $error = $ban->insert;
     if ($error) {
-      dbh->rollback if $oldAutoCommit;
+      dbh->rollback;
       return ( $error );
     }
 
@@ -2445,13 +2474,16 @@ sub cancel_pkgs {
                              'time'     => $cancel_time );
     if ($error) {
       warn "Error billing during cancel, custnum ". $self->custnum. ": $error";
-      dbh->rollback if $oldAutoCommit;
+      dbh->rollback;
       return ( "Error billing during cancellation: $error" );
     }
   }
+  dbh->commit;
 
+  $FS::UID::AutoCommit = 1;
   my @errors;
-  # now cancel all services, the same way we would for individual packages
+  # now cancel all services, the same way we would for individual packages.
+  # if any of them fail, cancel the rest anyway.
   my @cust_svc = map { $_->cust_svc } @pkgs;
   my @sorted_cust_svc =
     map  { $_->[0] }
@@ -2468,7 +2500,6 @@ sub cancel_pkgs {
     push @errors, $error if $error;
   }
   if (@errors) {
-    dbh->rollback if $oldAutoCommit;
     return @errors;
   }
 
@@ -2480,23 +2511,24 @@ sub cancel_pkgs {
   if ($opt{'cust_pkg_reason'}) {
     @cprs = @{ delete $opt{'cust_pkg_reason'} };
   }
+  my $null_reason;
   foreach (@pkgs) {
     my %lopt = %opt;
     if (@cprs) {
       my $cpr = shift @cprs;
-      $lopt{'reason'}        = $cpr->reasonnum;
-      $lopt{'reason_otaker'} = $cpr->otaker;
+      if ( $cpr ) {
+        $lopt{'reason'}        = $cpr->reasonnum;
+        $lopt{'reason_otaker'} = $cpr->otaker;
+      } else {
+        warn "no reason found when canceling package ".$_->pkgnum."\n";
+        $lopt{'reason'} = '';
+      }
     }
     my $error = $_->cancel(%lopt);
     push @errors, 'pkgnum '.$_->pkgnum.': '.$error if $error;
   }
 
-  if (@errors) {
-    dbh->rollback if $oldAutoCommit;
-    return @errors;
-  }
-
-  return;
+  return @errors;
 }
 
 sub _banned_pay_hashref {
@@ -4971,15 +5003,10 @@ Returns an SQL expression identifying un-cancelled cust_main records.
 =cut
 
 sub uncancelled_sql { uncancel_sql(@_); }
-sub uncancel_sql { "
-  ( 0 < ( $select_count_pkgs
-                   AND ( cust_pkg.cancel IS NULL
-                         OR cust_pkg.cancel = 0
-                       )
-        )
-    OR 0 = ( $select_count_pkgs )
-  )
-"; }
+sub uncancel_sql {
+  my $self = shift;
+  "( NOT (".$self->cancelled_sql.") )"; #sensitive to cust_main-status_module
+}
 
 =item balance_sql