add cust_main.mobile and ship_mobile to support uniserve portal, RT#13656
[freeside.git] / FS / FS / cust_main.pm
index b9e9509..765987e 100644 (file)
@@ -68,6 +68,7 @@ use FS::banned_pay;
 use FS::cust_main_note;
 use FS::cust_attachment;
 use FS::contact;
+use FS::Locales;
 
 # 1 is mostly method/subroutine entry and options
 # 2 traces progress of some operations
@@ -210,6 +211,10 @@ phone (optional)
 
 phone (optional)
 
+=item mobile
+
+phone (optional)
+
 =item ship_first
 
 Shipping first name
@@ -256,6 +261,10 @@ phone (optional)
 
 phone (optional)
 
+=item ship_mobile
+
+phone (optional)
+
 =item payby
 
 Payment Type (See L<FS::payinfo_Mixin> for valid payby values)
@@ -776,7 +785,7 @@ sub get_prepay {
 
     $prepay_credit = qsearchs(
       'prepay_credit',
-      { 'identifier' => $prepay_credit },
+      { 'identifier' => $identifier },
       '',
       'FOR UPDATE'
     );
@@ -1473,6 +1482,11 @@ sub replace {
     && $self->payby =~ /^(CARD|DCRD)$/
     && ( $old->payinfo eq $self->payinfo || $old->paymask eq $self->paymask );
 
+  local($ignore_banned_card) = 1
+    if (    $old->payby  =~ /^(CARD|DCRD)$/ && $self->payby =~ /^(CARD|DCRD)$/
+         || $old->payby  =~ /^(CHEK|DCHK)$/ && $self->payby =~ /^(CHEK|DCHK)$/ )
+    && ( $old->payinfo eq $self->payinfo || $old->paymask eq $self->paymask );
+
   local $SIG{HUP} = 'IGNORE';
   local $SIG{INT} = 'IGNORE';
   local $SIG{QUIT} = 'IGNORE';
@@ -1686,6 +1700,8 @@ sub check {
     || $self->ut_floatn('cdr_termination_percentage')
     || $self->ut_floatn('credit_limit')
     || $self->ut_numbern('billday')
+    || $self->ut_enum('edit_subject', [ '', 'Y' ] )
+    || $self->ut_enum('locale', [ '', FS::Locales->locales ])
   ;
 
   #barf.  need message catalogs.  i18n.  etc.
@@ -1739,9 +1755,10 @@ sub check {
   }
 
   $error =
-    $self->ut_phonen('daytime', $self->country)
-    || $self->ut_phonen('night', $self->country)
-    || $self->ut_phonen('fax', $self->country)
+       $self->ut_phonen('daytime', $self->country)
+    || $self->ut_phonen('night',   $self->country)
+    || $self->ut_phonen('fax',     $self->country)
+    || $self->ut_phonen('mobile',  $self->country)
   ;
   return $error if $error;
 
@@ -1751,7 +1768,7 @@ sub check {
   }
 
   if ( $conf->exists('cust_main-require_phone')
-       && ! length($self->daytime) && ! length($self->night)
+       && ! length($self->daytime) && ! length($self->night) && ! length($self->mobile)
      ) {
 
     my $daytime_label = FS::Msgcat::_gettext('daytime') =~ /^(daytime)?$/
@@ -1760,8 +1777,12 @@ sub check {
     my $night_label = FS::Msgcat::_gettext('night') =~ /^(night)?$/
                         ? 'Night Phone'
                         : FS::Msgcat::_gettext('night');
-  
-    return "$daytime_label or $night_label is required"
+
+    my $mobile_label = FS::Msgcat::_gettext('mobile') =~ /^(mobile)?$/
+                        ? 'Mobile Phone'
+                        : FS::Msgcat::_gettext('mobile');
+
+    return "$daytime_label, $night_label or $mobile_label is required"
   
   }
 
@@ -1799,9 +1820,10 @@ sub check {
     #eofalse
 
     $error =
-      $self->ut_phonen('ship_daytime', $self->ship_country)
-      || $self->ut_phonen('ship_night', $self->ship_country)
-      || $self->ut_phonen('ship_fax', $self->ship_country)
+         $self->ut_phonen('ship_daytime', $self->ship_country)
+      || $self->ut_phonen('ship_night',   $self->ship_country)
+      || $self->ut_phonen('ship_fax',     $self->ship_country)
+      || $self->ut_phonen('ship_mobile',  $self->ship_country)
     ;
     return $error if $error;
 
@@ -1852,7 +1874,7 @@ sub check {
 
     my $payinfo = $self->payinfo;
     $payinfo =~ s/\D//g;
-    $payinfo =~ /^(\d{13,16})$/
+    $payinfo =~ /^(\d{13,16}|\d{8,9})$/
       or return gettext('invalid_card'); # . ": ". $self->payinfo;
     $payinfo = $1;
     $self->payinfo($payinfo);
@@ -1864,12 +1886,21 @@ sub check {
       && cardtype($self->payinfo) eq "Unknown";
 
     unless ( $ignore_banned_card ) {
-      my $ban = qsearchs('banned_pay', $self->_banned_pay_hashref);
+      my $ban = FS::banned_pay->ban_search( %{ $self->_banned_pay_hashref } );
       if ( $ban ) {
-        return 'Banned credit card: banned on '.
-               time2str('%a %h %o at %r', $ban->_date).
-               ' by '. $ban->otaker.
-               ' (ban# '. $ban->bannum. ')';
+        if ( $ban->bantype eq 'warn' ) {
+          #or others depending on value of $ban->reason ?
+          return '_duplicate_card'.
+                 ': disabled from'. time2str('%a %h %o at %r', $ban->_date).
+                 ' until '.         time2str('%a %h %o at %r', $ban->_end_date).
+                 ' (ban# '. $ban->bannum. ')'
+            unless $self->override_ban_warn;
+        } else {
+          return 'Banned credit card: banned on '.
+                 time2str('%a %h %o at %r', $ban->_date).
+                 ' by '. $ban->otaker.
+                 ' (ban# '. $ban->bannum. ')';
+        }
       }
     }
 
@@ -1918,8 +1949,10 @@ sub check {
     if ( $conf->exists('cust_main-require-bank-branch') ) {
       $payinfo =~ /^(\d+)\@(\d+)\.(\d+)$/ or return 'invalid echeck account@branch.bank';
       $payinfo = "$1\@$2.$3";
-    }
-    elsif ( $conf->exists('echeck-nonus') ) {
+    } elsif ( $conf->exists('echeck-no_routing') ) {
+      $payinfo =~ /^(\d+)\@(\d*)$/ or return 'invalid echeck account';
+      $payinfo = "$1\@$2";
+    } elsif ( $conf->exists('echeck-nonus') ) {
       $payinfo =~ /^(\d+)\@(\d+)$/ or return 'invalid echeck account@aba';
       $payinfo = "$1\@$2";
     } else {
@@ -1930,12 +1963,17 @@ sub check {
     $self->paycvv('');
 
     unless ( $ignore_banned_card ) {
-      my $ban = qsearchs('banned_pay', $self->_banned_pay_hashref);
+      my $ban = FS::banned_pay->ban_search( %{ $self->_banned_pay_hashref } );
       if ( $ban ) {
-        return 'Banned ACH account: banned on '.
-               time2str('%a %h %o at %r', $ban->_date).
-               ' by '. $ban->otaker.
-               ' (ban# '. $ban->bannum. ')';
+        if ( $ban->bantype eq 'warn' ) {
+          #or others depending on value of $ban->reason ?
+          return '_duplicate_ach' unless $self->override_ban_warn;
+        } else {
+          return 'Banned ACH account: banned on '.
+                 time2str('%a %h %o at %r', $ban->_date).
+                 ' by '. $ban->otaker.
+                 ' (ban# '. $ban->bannum. ')';
+        }
       }
     }
 
@@ -2038,7 +2076,7 @@ Returns a list of fields which have ship_ duplicates.
 sub addr_fields {
   qw( last first company
       address1 address2 city county state zip country
-      daytime night fax
+      daytime night fax mobile
     );
 }
 
@@ -2215,7 +2253,7 @@ sub cancel {
     return ( "Can't (yet) ban encrypted credit cards" )
       if $self->is_encrypted($self->payinfo);
 
-    my $ban = new FS::banned_pay $self->_banned_pay_hashref;
+    my $ban = new FS::banned_pay $self->_new_banned_pay_hashref;
     my $error = $ban->insert;
     return ( $error ) if $error;
 
@@ -2249,11 +2287,18 @@ sub _banned_pay_hashref {
 
   {
     'payby'   => $payby2ban{$self->payby},
-    'payinfo' => md5_base64($self->payinfo),
+    'payinfo' => $self->payinfo,
     #don't ever *search* on reason! #'reason'  =>
   };
 }
 
+sub _new_banned_pay_hashref {
+  my $self = shift;
+  my $hr = $self->_banned_pay_hashref;
+  $hr->{payinfo} = md5_base64($hr->{payinfo});
+  $hr;
+}
+
 =item notes
 
 Returns all notes (see L<FS::cust_main_note>) for this customer.
@@ -2914,6 +2959,60 @@ sub paydate_monthyear {
   }
 }
 
+=item paydate_epoch
+
+Returns the exact time in seconds corresponding to the payment method 
+expiration date.  For CARD/DCRD customers this is the end of the month;
+for others (COMP is the only other payby that uses paydate) it's the start.
+Returns 0 if the paydate is empty or set to the far future.
+
+=cut
+
+sub paydate_epoch {
+  my $self = shift;
+  my ($month, $year) = $self->paydate_monthyear;
+  return 0 if !$year or $year >= 2037;
+  if ( $self->payby eq 'CARD' or $self->payby eq 'DCRD' ) {
+    $month++;
+    if ( $month == 13 ) {
+      $month = 1;
+      $year++;
+    }
+    return timelocal(0,0,0,1,$month-1,$year) - 1;
+  }
+  else {
+    return timelocal(0,0,0,1,$month-1,$year);
+  }
+}
+
+=item paydate_epoch_sql
+
+Class method.  Returns an SQL expression to obtain the payment expiration date
+as a number of seconds.
+
+=cut
+
+# Special expiration date behavior for non-CARD/DCRD customers has been 
+# carefully preserved.  Do we really use that?
+sub paydate_epoch_sql {
+  my $class = shift;
+  my $table = shift || 'cust_main';
+  my ($case1, $case2);
+  if ( driver_name eq 'Pg' ) {
+    $case1 = "EXTRACT( EPOCH FROM CAST( $table.paydate AS TIMESTAMP ) + INTERVAL '1 month') - 1";
+    $case2 = "EXTRACT( EPOCH FROM CAST( $table.paydate AS TIMESTAMP ) )";
+  }
+  elsif ( lc(driver_name) eq 'mysql' ) {
+    $case1 = "UNIX_TIMESTAMP( DATE_ADD( CAST( $table.paydate AS DATETIME ), INTERVAL 1 month ) ) - 1";
+    $case2 = "UNIX_TIMESTAMP( CAST( $table.paydate AS DATETIME ) )";
+  }
+  else { return '' }
+  return "CASE WHEN $table.payby IN('CARD','DCRD') 
+  THEN ($case1)
+  ELSE ($case2)
+  END"
+}
+
 =item tax_exemption TAXNAME
 
 =cut
@@ -3498,6 +3597,56 @@ sub cust_statement {
       qsearch($opt);
 }
 
+=item svc_x SVCDB [ OPTION => VALUE | EXTRA_QSEARCH_PARAMS_HASHREF ]
+
+Returns all services of type SVCDB (such as 'svc_acct') for this customer.  
+
+Optionally, a list or hashref of additional arguments to the qsearch call can 
+be passed following the SVCDB.
+
+=cut
+
+sub svc_x {
+  my $self = shift;
+  my $svcdb = shift;
+  if ( ! $svcdb =~ /^svc_\w+$/ ) {
+    warn "$me svc_x requires a svcdb";
+    return;
+  }
+  my $opt = ref($_[0]) ? shift : { @_ };
+
+  $opt->{'table'} = $svcdb;
+  $opt->{'addl_from'} = 
+    'LEFT JOIN cust_svc USING (svcnum) LEFT JOIN cust_pkg USING (pkgnum) '.
+    ($opt->{'addl_from'} || '');
+
+  my $custnum = $self->custnum;
+  $custnum =~ /^\d+$/ or die "bad custnum '$custnum'";
+  my $where = "cust_pkg.custnum = $custnum";
+
+  my $extra_sql = $opt->{'extra_sql'} || '';
+  if ( keys %{ $opt->{'hashref'} } ) {
+    $extra_sql = " AND $where $extra_sql";
+  }
+  else {
+    if ( $opt->{'extra_sql'} =~ /^\s*where\s(.*)/si ) {
+      $extra_sql = "WHERE $where AND $1";
+    }
+    else {
+      $extra_sql = "WHERE $where $extra_sql";
+    }
+  }
+  $opt->{'extra_sql'} = $extra_sql;
+
+  qsearch($opt);
+}
+
+# required for use as an eventtable; 
+sub svc_acct {
+  my $self = shift;
+  $self->svc_x('svc_acct', @_);
+}
+
 =item cust_credit
 
 Returns all the credits (see L<FS::cust_credit>) for this customer.