[freeside-commits] branch FREESIDE_3_BRANCH updated. 2787aff7551116a223b4dea8bb144c1d03452d01

Mark Wells mark at 420.am
Thu Aug 11 16:17:41 PDT 2016


The branch, FREESIDE_3_BRANCH has been updated
       via  2787aff7551116a223b4dea8bb144c1d03452d01 (commit)
       via  267fab684da71cbe17dcd78d16283cb9b0f8064f (commit)
       via  a238acfb85cd4bef6a99bfe3560a0999c9386dfb (commit)
       via  a21dee1f18ad9ac220efe66a0c044aa262a12c99 (commit)
       via  8d08e8194a4f57391eb381fbd541fb3e5e25c0dd (commit)
       via  dd9a0ea1c8351841c8d41ab46e94abbdb0c75db4 (commit)
       via  12ff9cacb95d18ee0398c85f9e71ce8f21940136 (commit)
       via  27b0b17db98192df66fc51cc8f27dc7cc1b4ab8e (commit)
       via  77e088ce10a52b40743ff9ae7e82336f167db2f8 (commit)
       via  050e311ccb5ef9747a87632842081b99453aba4b (commit)
      from  4c082cdd608ba5b1e95602bb7069259f25e76916 (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.

- Log -----------------------------------------------------------------
commit 2787aff7551116a223b4dea8bb144c1d03452d01
Author: Mark Wells <mark at freeside.biz>
Date:   Thu Aug 11 16:16:18 2016 -0700

    skip paycardtype upgrades on 3.x, since we don't use it yet, #71291

diff --git a/FS/FS/cust_pay.pm b/FS/FS/cust_pay.pm
index 04aac4e..d45d2e3 100644
--- a/FS/FS/cust_pay.pm
+++ b/FS/FS/cust_pay.pm
@@ -1248,9 +1248,9 @@ sub _upgrade_data {  #class method
   }
 
   ###
-  # set paycardtype
+  # don't set paycardtype until 4.x
   ###
-  $class->upgrade_set_cardtype;
+  #$class->upgrade_set_cardtype;
 
 }
 
diff --git a/FS/FS/cust_refund.pm b/FS/FS/cust_refund.pm
index e26121b..4affb15 100644
--- a/FS/FS/cust_refund.pm
+++ b/FS/FS/cust_refund.pm
@@ -522,7 +522,9 @@ sub _upgrade_data {  # class method
   $class->_upgrade_otaker(%opts);
 
   local $ignore_empty_reasonnum = 1;
-  $class->upgrade_set_cardtype;
+
+  # don't set paycardtype until 4.x
+  #$class->upgrade_set_cardtype;
 }
 
 =back

commit 267fab684da71cbe17dcd78d16283cb9b0f8064f
Author: Mark Wells <mark at freeside.biz>
Date:   Thu Aug 11 16:02:00 2016 -0700

    debug

diff --git a/httemplate/search/sqlradius_usage.html b/httemplate/search/sqlradius_usage.html
index b5551ad..fcf6c10 100644
--- a/httemplate/search/sqlradius_usage.html
+++ b/httemplate/search/sqlradius_usage.html
@@ -176,7 +176,6 @@ if ( exists($opt{usage_by_username}) ) {
   }
 }
 
-#warn Dumper(\%usage_by_username);
 my @total_usage = (0, 0, 0, 0); # session time, input, output, input + output
 my @svc_usage = map {
   my $i = $_;
@@ -257,7 +256,4 @@ sub bytes_to_gb {
   $_[0] ?  sprintf('%.3f', $_[0] / (1024*1024*1024.0)) : '';
 }
 
-warn Dumper \%usage_by_username;
-
-
 </%init>

commit a238acfb85cd4bef6a99bfe3560a0999c9386dfb
Author: Mark Wells <mark at freeside.biz>
Date:   Mon Aug 8 13:23:25 2016 -0700

    option to extract destination number from userfield, #71674

diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm
index 587b2a2..d09212d 100644
--- a/FS/FS/Conf.pm
+++ b/FS/FS/Conf.pm
@@ -5321,6 +5321,13 @@ and customer address. Include units.',
   },
 
   {
+    'key'         => 'cdr-userfield_dnis_rewrite',
+    'section'     => 'telephony',
+    'description' => 'If the CDR userfield contains "DNIS=" followed by a sequence of digits, use that as the destination number for the call.',
+    'type'        => 'checkbox',
+  },
+
+  {
     'key'         => 'cdr-intl_to_domestic_rewrite',
     'section'     => 'telephony',
     'description' => 'Strip the "011" international prefix from CDR destination numbers if the rest of the number is 7 digits or shorter, and so probably does not contain a country code.',
diff --git a/FS/bin/freeside-cdrrewrited b/FS/bin/freeside-cdrrewrited
index 0087590..f9d97af 100644
--- a/FS/bin/freeside-cdrrewrited
+++ b/FS/bin/freeside-cdrrewrited
@@ -198,6 +198,12 @@ while (1) {
 
     }
 
+    if ( $conf->exists('cdr-userfield_dnis_rewrite') and
+         $cdr->userfield =~ /DNIS=(\d+)/ ) {
+      $cdr->dst($1);
+      push @status, 'userfield_dnis';
+    }
+
     if ( $conf->exists('cdr-intl_to_domestic_rewrite') and
          $cdr->dst =~ /^(011)(\d{0,7})$/ ) {
       $cdr->dst($2);
@@ -240,6 +246,7 @@ sub _shouldrun {
   || $conf->exists('cdr-taqua-accountcode_rewrite')
   || $conf->exists('cdr-taqua-callerid_rewrite')
   || $conf->exists('cdr-intl_to_domestic_rewrite')
+  || $conf->exists('cdr-userfield_dnis_rewrite')
   || 0
   ;
 }

commit a21dee1f18ad9ac220efe66a0c044aa262a12c99
Author: Mark Wells <mark at freeside.biz>
Date:   Tue Jul 26 12:08:59 2016 -0700

    customer menu link to data usage report, #42310

diff --git a/httemplate/view/cust_main/packages.html b/httemplate/view/cust_main/packages.html
index 9a2332b..f3cfc06 100755
--- a/httemplate/view/cust_main/packages.html
+++ b/httemplate/view/cust_main/packages.html
@@ -167,6 +167,17 @@ if ( el ) el.scrollIntoView(true);
         <A HREF="<%$p%>search/report_svc_acct.html?custnum=<% $cust_main->custnum %>"><% mt('accounts') |h %></A><BR>
       <% mt('Usage reports:') |h %> 
         <A HREF="<%$p%>search/report_cdr.html?custnum=<% $cust_main->custnum %>"><% mt('CDRs') |h %></A>
+%      if ( $curuser->access_right('Usage: RADIUS sessions') ) {
+       |
+     <& /elements/popup_link-cust_main.html,
+                 'label'       => emt('Data usage'),
+                 'action'      => "${p}search/report_sqlradius_usage-custnum.html",
+                 'cust_main'   => $cust_main,
+                 'actionlabel' => emt('Data usage report'),
+                 'width'       => 480,
+                 'height'      => 245,
+     &>
+%      }
     </TD>
 
 %   } # unless $opt{no_links}

commit 8d08e8194a4f57391eb381fbd541fb3e5e25c0dd
Author: Mark Wells <mark at freeside.biz>
Date:   Tue Jul 26 11:49:18 2016 -0700

    per-customer RADIUS data usage report, #42310

diff --git a/FS/FS/svc_Common.pm b/FS/FS/svc_Common.pm
index d11b583..229cfdd 100644
--- a/FS/FS/svc_Common.pm
+++ b/FS/FS/svc_Common.pm
@@ -1488,8 +1488,12 @@ sub search {
   }
 
   #svcnum
-  if ( $params->{'svcnum'} =~ /^(\d+)$/ ) {
-    push @where, "svcnum = $1";
+  if ( $params->{'svcnum'} ) {
+    my @svcnum = ref( $params->{'svcnum'} )
+                 ? @{ $params->{'svcnum'} }
+                 : $params->{'svcnum'};
+    @svcnum = grep /^\d+$/, @svcnum;
+    push @where, 'svcnum IN ('. join(',', @svcnum) . ')' if @svcnum;
   }
 
   # svcpart
diff --git a/httemplate/search/report_sqlradius_usage-custnum.html b/httemplate/search/report_sqlradius_usage-custnum.html
new file mode 100644
index 0000000..a71012d
--- /dev/null
+++ b/httemplate/search/report_sqlradius_usage-custnum.html
@@ -0,0 +1,71 @@
+<& /elements/header-popup.html, mt($title) &>
+
+<FORM ACTION="sqlradius_usage.html" METHOD="GET" TARGET="_top">
+
+<& /elements/hidden.html,
+  'field' => 'custnum',
+  'value' => $custnum,
+&>
+<TABLE BGCOLOR="#cccccc" CELLSPACING=0>
+
+% if ( scalar(@exports) == 1 ) {
+<tr><td>
+<& /elements/hidden.html,
+  'field'         => 'exportnum',
+  'value'         => $exports[0]->exportnum,
+&>
+</td></tr>
+% } else {
+<& /elements/tr-select-table.html,
+  'label'         => 'Export', # kind of non-indicative...
+  'table'         => 'part_export',
+  'name_col'      => 'label',
+  'value_col'     => 'exportnum',
+  'records'       => \@exports,
+  'disable_empty' => 1,
+&>
+% }
+<& /elements/tr-input-beginning_ending.html &>
+
+</TABLE>
+
+<BR>
+<INPUT TYPE="submit" VALUE="<% mt('Get Report') |h %>">
+
+</FORM>
+
+<& /elements/footer.html &>
+<%init>
+
+my $curuser = $FS::CurrentUser::CurrentUser;
+die "access denied"
+  unless $curuser->access_right('Usage: RADIUS sessions');
+  # yes?
+
+my $title = 'Data Usage Report';
+my $custnum;
+if ($cgi->keywords) {
+  ($custnum) = $cgi->keywords;
+} else {
+  $custnum = $cgi->param('custnum');
+}
+$custnum =~ /^(\d+)$/
+  or die "illegal custnum $custnum";
+my $cust_main = qsearchs( {
+  'table'     => 'cust_main',
+  'hashref'   => { 'custnum' => $custnum },
+  'extra_sql' => ' AND '. $curuser->agentnums_sql,
+});
+# get all exports that apply to this customer's services--should be fast, as
+# everything here is indexed
+my @exports = qsearch({
+  'table'     => 'part_export',
+  'select'    => 'DISTINCT part_export.*',
+  'addl_from' => ' JOIN export_svc USING (exportnum)
+                   JOIN cust_svc USING (svcpart)
+                   JOIN cust_pkg USING (pkgnum) ',
+  'extra_sql' => ' WHERE cust_pkg.custnum = '.$custnum,
+});
+ at exports = grep { $_->can('usage_sessions') } @exports;
+
+</%init>
diff --git a/httemplate/search/report_sqlradius_usage.html b/httemplate/search/report_sqlradius_usage.html
index e818fb5..89b6084 100644
--- a/httemplate/search/report_sqlradius_usage.html
+++ b/httemplate/search/report_sqlradius_usage.html
@@ -1,3 +1,4 @@
+%# some overlap with report_sqlradius_usage_custnum.html
 <& /elements/header.html, mt($title) &>
 
 <FORM ACTION="sqlradius_usage.html" METHOD="GET">
diff --git a/httemplate/search/sqlradius_usage.html b/httemplate/search/sqlradius_usage.html
index 29ef4c0..b5551ad 100644
--- a/httemplate/search/sqlradius_usage.html
+++ b/httemplate/search/sqlradius_usage.html
@@ -59,8 +59,8 @@
 
 my %opt = @_;
 
-die "access denied" unless
-  $FS::CurrentUser::CurrentUser->access_right('List services');
+my $curuser = $FS::CurrentUser::CurrentUser;
+die "access denied" unless $curuser->access_right('List services');
 
 my $title = 'Data Usage Report - '; 
 my $agentnum;
@@ -92,6 +92,40 @@ if ( $ending == 4294967295 ) {
   $title .= time2str('%h %o %Y', $ending);
 }
 
+# can also show a specific customer / service. the main query will handle
+# agent restrictions, but we need a list of the services to ask the export
+# for usage data.
+my ($cust_main, @svc_x);
+if ( $cgi->param('custnum') =~ /^(\d+)$/ ) {
+  $cust_main = qsearchs( {
+    'table'     => 'cust_main',
+    'hashref'   => { 'custnum' => $1 },
+    'extra_sql' => ' AND '. $curuser->agentnums_sql,
+  });
+  die "Customer not found!" unless $cust_main;
+  # then only report on this agent
+  $agentnum = $cust_main->agentnum;
+  @include_agents = ();
+  # and announce that we're doing it
+  $title .= ' - ' . $cust_main->name_short;
+
+  # yes, we'll query the database once for each service the customer has,
+  # even non-radacct'd services. probably less bad than a single query that
+  # pulls records for every service for every customer.
+  foreach my $cust_pkg ($cust_main->all_pkgs) {
+    foreach my $cust_svc ($cust_pkg->cust_svc) {
+      push @svc_x, $cust_svc->svc_x;
+    }
+  }
+}
+foreach ($cgi->param('svcnum')) {
+  if (/^(\d+)$/) {
+    my $cust_svc = FS::cust_svc->by_key($1)
+      or die "service #$1 not found."; # or continue?
+    push @svc_x, $cust_svc->svc_x;
+  }
+}
+
 my $export;
 my %usage_by_username;
 if ( exists($opt{usage_by_username}) ) {
@@ -109,16 +143,28 @@ if ( exists($opt{usage_by_username}) ) {
     or die "exportnum ".$export->exportnum." is type ".$export->exporttype.
            ", not sqlradius";
 
-  my $usage = $export->usage_sessions( {
+  my %usage_param = (
       stoptime_start  => $beginning,
       stoptime_end    => $ending,
       summarize       => 1
-  } );
-  # arrayref of hashrefs of
+  );
+  # usage_sessions() returns an arrayref of hashrefs of
   # (username, acctsessiontime, acctinputoctets, acctoutputoctets)
   # (XXX needs to include 'realm' for sqlradius_withdomain)
-  # rearrange to be indexed by username.
+  my $usage;
+  if ( @svc_x ) {
+    # then query once per service
+    $usage = [];
+    foreach my $svc ( @svc_x ) {
+      $usage_param{'svc'} = $svc;
+      push @$usage, @{ $export->usage_sessions(\%usage_param) };
+    }
+  } else {
+    # one query, get everyone's data
+    my $usage = $export->usage_sessions(\%usage_param);
+  }
 
+  # rearrange to be indexed by username.
   foreach (@$usage) {
     my $username = $_->{'username'};
     my @row = (
@@ -171,10 +217,22 @@ my @svc_fields = @{ $svc_fields{$svcdb} };
 my %search_hash = ( 'agentnum' => $agentnum,
                     'exportnum' => $export->exportnum );
 
+if ($cust_main) {
+  $search_hash{'custnum'} = $cust_main->custnum;
+}
+if (@svc_x) {
+  $search_hash{'svcnum'} = [ map { $_->get('svcnum') } @svc_x ];
+}
+
 my $sql_query = $class->search(\%search_hash);
 $sql_query->{'select'}    .= ', part_pkg.pkg';
 $sql_query->{'addl_from'} .= ' LEFT JOIN part_pkg USING (pkgpart)';
 
+if ( @svc_x ) {
+  my $svcnums = join(',', map { $_->get('svcnum') } @svc_x);
+  $sql_query->{'extra_sql'} .= ' AND svcnum IN('.$svcnums.')';
+}
+
 my $link_svc = [ $p.'view/cust_svc.cgi?', 'svcnum' ];
 
 my $link_cust = [ $p.'view/cust_main.cgi?', 'custnum' ];
@@ -182,9 +240,10 @@ my $link_cust = [ $p.'view/cust_main.cgi?', 'custnum' ];
 # columns between the customer name and the usage fields
 my $skip_cols = 1 + scalar(@svc_header);
 
+my $num_rows = FS::Record->scalar_sql($sql_query->{count_query});
 my @footer = (
   '',
-  FS::Record->scalar_sql($sql_query->{count_query}) . ' services',
+  emt('[quant,_1,service]', $num_rows), 
   ('') x $skip_cols,
   map {
     my $i = $_;
@@ -198,4 +257,7 @@ sub bytes_to_gb {
   $_[0] ?  sprintf('%.3f', $_[0] / (1024*1024*1024.0)) : '';
 }
 
+warn Dumper \%usage_by_username;
+
+
 </%init>

commit dd9a0ea1c8351841c8d41ab46e94abbdb0c75db4
Author: Mark Wells <mark at freeside.biz>
Date:   Thu Jul 21 13:47:42 2016 -0700

    fix whitespace and case correctness of city names, #71501

diff --git a/FS/FS/Record.pm b/FS/FS/Record.pm
index 55cca08..7e9a2e0 100644
--- a/FS/FS/Record.pm
+++ b/FS/FS/Record.pm
@@ -3034,6 +3034,22 @@ sub ut_agentnum_acl {
 
 }
 
+=item trim_whitespace FIELD[, FIELD ... ]
+
+Strip leading and trailing spaces from the value in the named FIELD(s).
+
+=cut
+
+sub trim_whitespace {
+  my $self = shift;
+  foreach my $field (@_) {
+    my $value = $self->get($field);
+    $value =~ s/^\s+//;
+    $value =~ s/\s+$//;
+    $self->set($field, $value);
+  }
+}
+
 =item fields [ TABLE ]
 
 This is a wrapper for real_fields.  Code that called
diff --git a/FS/FS/Upgrade.pm b/FS/FS/Upgrade.pm
index 377a0b1..5b27505 100644
--- a/FS/FS/Upgrade.pm
+++ b/FS/FS/Upgrade.pm
@@ -441,8 +441,12 @@ sub upgrade_data {
     #set default locations on quoted packages
     'quotation_pkg' => [],
 
-    #mark certain taxes as system-maintained
+    #mark certain taxes as system-maintained,
+    # and fix whitespace
     'cust_main_county' => [],
+
+    #fix whitespace
+    'cust_location' => [],
   ;
 
   \%hash;
diff --git a/FS/FS/cust_location.pm b/FS/FS/cust_location.pm
index 4960f74..481ebb1 100644
--- a/FS/FS/cust_location.pm
+++ b/FS/FS/cust_location.pm
@@ -2,7 +2,7 @@ package FS::cust_location;
 use base qw( FS::geocode_Mixin FS::Record );
 
 use strict;
-use vars qw( $import $DEBUG $conf $label_prefix );
+use vars qw( $import $DEBUG $conf $label_prefix $allow_location_edit );
 use Data::Dumper;
 use Date::Format qw( time2str );
 use FS::UID qw( dbh driver_name );
@@ -166,6 +166,10 @@ sub find_or_insert {
   delete $nonempty{'locationnum'};
 
   my %hash = map { $_ => $self->get($_) } @essential;
+  foreach (values %hash) {
+    s/^\s+//;
+    s/\s+$//;
+  }
   my @matches = qsearch('cust_location', \%hash);
 
   # we no longer reject matches for having different values in nonessential
@@ -287,7 +291,7 @@ sub replace {
   # it's a prospect location, then there are no active packages, no billing
   # history, no taxes, and in general no reason to keep the old location
   # around.
-  if ( $self->custnum ) {
+  if ( !$allow_location_edit and $self->custnum ) {
     foreach (qw(address1 address2 city state zip country)) {
       if ( $self->$_ ne $old->$_ ) {
         return "can't change cust_location field $_";
@@ -342,6 +346,10 @@ sub check {
 
   return '' if $self->disabled; # so that disabling locations never fails
 
+  # maybe should just do all fields in the table?
+  # or in every table?
+  $self->trim_whitespace(qw(district city county state country));
+
   my $error = 
     $self->ut_numbern('locationnum')
     || $self->ut_foreign_keyn('prospectnum', 'prospect_main', 'prospectnum')
@@ -891,6 +899,35 @@ sub process_standardize {
   close $log;
 }
 
+sub _upgrade_data {
+  my $class = shift;
+
+  # are we going to need to update tax districts?
+  my $use_districts = $conf->config('tax_district_method') ? 1 : 0;
+
+  # trim whitespace on records that need it
+  local $allow_location_edit = 1;
+  foreach my $field (qw(city county state country district)) {
+    foreach my $location (qsearch({
+      table => 'cust_location',
+      extra_sql => " WHERE $field LIKE ' %' OR $field LIKE '% '"
+    })) {
+      my $error = $location->replace;
+      die "$error (fixing whitespace in $field, locationnum ".$location->locationnum.')'
+        if $error;
+
+      if ( $use_districts ) {
+        my $queue = new FS::queue {
+          'job' => 'FS::geocode_Mixin::process_district_update'
+        };
+        $error = $queue->insert( 'FS::cust_location' => $location->locationnum );
+        die $error if $error;
+      }
+    } # foreach $location
+  } # foreach $field
+  '';
+}
+
 =head1 BUGS
 
 =head1 SEE ALSO
diff --git a/FS/FS/cust_main/Billing.pm b/FS/FS/cust_main/Billing.pm
index d6a765a..b278fe4 100644
--- a/FS/FS/cust_main/Billing.pm
+++ b/FS/FS/cust_main/Billing.pm
@@ -1767,8 +1767,10 @@ sub _handle_taxes {
     # We fetch taxes even if the customer is completely exempt,
     # because we need to record that fact.
 
-    my @loc_keys = qw( district city county state country );
-    my %taxhash = map { $_ => $location->$_ } @loc_keys;
+    my %taxhash = map { $_ => $location->get($_) }
+                  qw( district county state country );
+    # city names in cust_main_county are uppercase
+    $taxhash{'city'} = uc($location->get('city'));
 
     $taxhash{'taxclass'} = $part_item->taxclass;
 
diff --git a/FS/FS/cust_main_county.pm b/FS/FS/cust_main_county.pm
index 6eadff2..40caabb 100644
--- a/FS/FS/cust_main_county.pm
+++ b/FS/FS/cust_main_county.pm
@@ -122,6 +122,9 @@ methods.
 sub check {
   my $self = shift;
 
+  $self->trim_whitespace(qw(district city county state country));
+  $self->set('city', uc($self->get('city'))); # also county?
+
   $self->exempt_amount(0) unless $self->exempt_amount;
 
   $self->ut_numbern('taxnum')
@@ -660,6 +663,49 @@ sub _upgrade_data {
     }
     FS::upgrade_journal->set_done($journal);
   }
+  # trim whitespace and convert to uppercase in the 'city' field.
+  foreach my $record (qsearch({
+    table => 'cust_main_county',
+    extra_sql => " WHERE city LIKE ' %' OR city LIKE '% ' OR city != UPPER(city)",
+  })) {
+    # any with-trailing-space records probably duplicate other records
+    # from the same city, and if we just fix the record in place, we'll
+    # create an exact duplicate.
+    # so find the record this one would duplicate, and merge them.
+    $record->check; # trims whitespace
+    my %match = map { $_ => $record->get($_) }
+      qw(city county state country district taxname taxclass);
+    my $other = qsearchs('cust_main_county', \%match);
+    if ($other) {
+      my $new_taxnum = $other->taxnum;
+      my $old_taxnum = $record->taxnum;
+      if ($other->tax != $record->tax or
+          $other->exempt_amount != $record->exempt_amount) {
+        # don't assume these are the same.
+        warn "Found duplicate taxes (#$new_taxnum and #$old_taxnum) but they have different rates and can't be merged.\n";
+      } else {
+        warn "Merging tax #$old_taxnum into #$new_taxnum\n";
+        foreach my $table (qw(
+          cust_bill_pkg_tax_location
+          cust_bill_pkg_tax_location_void
+          cust_tax_exempt_pkg
+          cust_tax_exempt_pkg_void
+        )) {
+          foreach my $row (qsearch($table, { 'taxnum' => $old_taxnum })) {
+            $row->set('taxnum' => $new_taxnum);
+            my $error = $row->replace;
+            die $error if $error;
+          }
+        }
+        my $error = $record->delete;
+        die $error if $error;
+      }
+    } else {
+      # else there is no record this one duplicates, so just fix it
+      my $error = $record->replace;
+      die $error if $error;
+    }
+  } # foreach $record
   '';
 }
 

commit 12ff9cacb95d18ee0398c85f9e71ce8f21940136
Author: Mark Wells <mark at freeside.biz>
Date:   Tue Jul 19 12:56:06 2016 -0700

    unreverse the check for tokenized payinfo, #71291

diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm
index bd46307..8e2fa19 100644
--- a/FS/FS/cust_main.pm
+++ b/FS/FS/cust_main.pm
@@ -1967,7 +1967,7 @@ sub check {
       or return gettext('invalid_card'); # . ": ". $self->payinfo;
 
     my $cardtype = cardtype($payinfo);
-    $cardtype = 'Tokenized' if $self->payinfo !~ /^99\d{14}$/; # token
+    $cardtype = 'Tokenized' if $self->payinfo =~ /^99\d{14}$/; # token
 
     return gettext('unknown_card_type') if $cardtype eq 'Unknown';
 
diff --git a/FS/FS/payinfo_Mixin.pm b/FS/FS/payinfo_Mixin.pm
index b02e98e..4da40e3 100644
--- a/FS/FS/payinfo_Mixin.pm
+++ b/FS/FS/payinfo_Mixin.pm
@@ -201,7 +201,7 @@ sub payinfo_check {
 
     my $payinfo = $self->payinfo;
     my $cardtype = cardtype($payinfo);
-    $cardtype = 'Tokenized' if $payinfo !~ /^99\d{14}$/;
+    $cardtype = 'Tokenized' if $payinfo =~ /^99\d{14}$/;
     $self->set('paycardtype', $cardtype);
 
     if ( $ignore_masked_payinfo and $self->mask_payinfo eq $self->payinfo ) {

commit 27b0b17db98192df66fc51cc8f27dc7cc1b4ab8e
Author: Mark Wells <mark at freeside.biz>
Date:   Sat Jul 16 15:48:18 2016 -0700

    backport cust_payby changes onto cust_main instead

diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm
index 63c113c..a495667 100644
--- a/FS/FS/Schema.pm
+++ b/FS/FS/Schema.pm
@@ -1178,6 +1178,7 @@ sub tables_hashref {
         'ship_mobile',   'varchar', 'NULL', 12, '', '', 
         'payby',    'char', '',     4, '', '', 
         'payinfo',  'varchar', 'NULL', 512, '', '', 
+        'paycardtype', 'varchar', 'NULL',   $char_d, '', '',
         'paycvv',   'varchar', 'NULL', 512, '', '', 
         'paymask', 'varchar', 'NULL', $char_d, '', '', 
         #'paydate',  @date_type, '', '', 
diff --git a/FS/FS/Upgrade.pm b/FS/FS/Upgrade.pm
index 481646d..377a0b1 100644
--- a/FS/FS/Upgrade.pm
+++ b/FS/FS/Upgrade.pm
@@ -391,9 +391,6 @@ sub upgrade_data {
     'cust_refund' => [],
     'banned_pay' => [],
 
-    #paycardtype
-    'cust_payby' => [],
-
     #default namespace
     'payment_gateway' => [],
 
diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm
index 8496684..bd46307 100644
--- a/FS/FS/cust_main.pm
+++ b/FS/FS/cust_main.pm
@@ -239,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'
@@ -1962,9 +1966,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 } );
@@ -1986,7 +1993,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);
@@ -1999,7 +2006,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"
@@ -2096,6 +2102,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 '-' ) {
@@ -2168,10 +2179,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};
 

commit 77e088ce10a52b40743ff9ae7e82336f167db2f8
Author: Mark Wells <mark at freeside.biz>
Date:   Fri Jul 15 15:50:27 2016 -0700

    rename cardtype to paycardtype

diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm
index f3fca2e..63c113c 100644
--- a/FS/FS/Schema.pm
+++ b/FS/FS/Schema.pm
@@ -1755,7 +1755,7 @@ sub tables_hashref {
                                                  # index into payby table
                                                  # eventually
         'payinfo',  'varchar',   'NULL', 512, '', '', #see cust_main above
-        'cardtype',    'varchar', 'NULL',   $char_d, '', '',
+        'paycardtype',    'varchar', 'NULL',   $char_d, '', '',
         'paymask', 'varchar', 'NULL', $char_d, '', '', 
         'paydate',  'varchar', 'NULL', 10, '', '', 
         'paybatch', 'varchar',   'NULL', $char_d, '', '', #for auditing purposes.
@@ -1794,7 +1794,7 @@ sub tables_hashref {
                                                   # index into payby table
                                                   # eventually
         'payinfo',   'varchar',   'NULL', 512, '', '', #see cust_main above
-        'cardtype',    'varchar', 'NULL',   $char_d, '', '',
+        'paycardtype',    'varchar', 'NULL',   $char_d, '', '',
         'paymask', 'varchar', 'NULL', $char_d, '', '', 
         #'paydate' ?
         'paybatch',  'varchar',   'NULL', $char_d, '', '', #for auditing purposes.
@@ -2146,7 +2146,7 @@ sub tables_hashref {
                                                      # be index into payby
                                                      # table eventually
         'payinfo',      'varchar',   'NULL', 512, '', '', #see cust_main above
-        'cardtype',    'varchar', 'NULL',   $char_d, '', '',
+        'paycardtype',  'varchar', 'NULL',   $char_d, '', '',
         'paymask', 'varchar', 'NULL', $char_d, '', '', 
         'paybatch',     'varchar',   'NULL', $char_d, '', '', 
         'closed',    'char', 'NULL', 1, '', '', 
diff --git a/FS/FS/Upgrade.pm b/FS/FS/Upgrade.pm
index 26cd42a..481646d 100644
--- a/FS/FS/Upgrade.pm
+++ b/FS/FS/Upgrade.pm
@@ -391,7 +391,7 @@ sub upgrade_data {
     'cust_refund' => [],
     'banned_pay' => [],
 
-    #cardtype
+    #paycardtype
     'cust_payby' => [],
 
     #default namespace
diff --git a/FS/FS/cust_pay.pm b/FS/FS/cust_pay.pm
index 6676317..04aac4e 100644
--- a/FS/FS/cust_pay.pm
+++ b/FS/FS/cust_pay.pm
@@ -96,7 +96,7 @@ Payment Type (See L<FS::payinfo_Mixin> for valid values)
 
 Payment Information (See L<FS::payinfo_Mixin> for data format)
 
-=item cardtype
+=item paycardtype
 
 Credit card type, if appropriate; autodetected.
 
@@ -1248,7 +1248,7 @@ sub _upgrade_data {  #class method
   }
 
   ###
-  # set cardtype
+  # set paycardtype
   ###
   $class->upgrade_set_cardtype;
 
diff --git a/FS/FS/cust_refund.pm b/FS/FS/cust_refund.pm
index 17bd13d..e26121b 100644
--- a/FS/FS/cust_refund.pm
+++ b/FS/FS/cust_refund.pm
@@ -83,7 +83,7 @@ Payment Type (See L<FS::payinfo_Mixin> for valid payby values)
 
 Payment Information (See L<FS::payinfo_Mixin> for data format)
 
-=item cardtype
+=item paycardtype
 
 Detected credit card type, if appropriate; autodetected.
 
diff --git a/FS/FS/payinfo_Mixin.pm b/FS/FS/payinfo_Mixin.pm
index 81e04a4..b02e98e 100644
--- a/FS/FS/payinfo_Mixin.pm
+++ b/FS/FS/payinfo_Mixin.pm
@@ -198,9 +198,12 @@ sub payinfo_check {
     or return "Illegal payby: ". $self->payby;
 
   if ( $self->payby eq 'CARD' && ! $self->is_encrypted($self->payinfo) ) {
+
     my $payinfo = $self->payinfo;
     my $cardtype = cardtype($payinfo);
-    $self->set('cardtype', $cardtype);
+    $cardtype = 'Tokenized' if $payinfo !~ /^99\d{14}$/;
+    $self->set('paycardtype', $cardtype);
+
     if ( $ignore_masked_payinfo and $self->mask_payinfo eq $self->payinfo ) {
       # allow it
     } else {
@@ -211,8 +214,7 @@ sub payinfo_check {
           or return "Illegal (mistyped?) credit card number (payinfo)";
         $self->payinfo($1);
         validate($self->payinfo) or return "Illegal credit card number";
-        return "Unknown card type" if $self->payinfo !~ /^99\d{14}$/ #token
-                                   && $cardtype eq "Unknown";
+        return "Unknown card type" if $cardtype eq "Unknown";
       } else {
         $self->payinfo('N/A'); #???
       }
@@ -220,9 +222,9 @@ sub payinfo_check {
   } else {
     if ( $self->payby eq 'CARD' and $self->paymask ) {
       # if we can't decrypt the card, at least detect the cardtype
-      $self->set('cardtype', cardtype($self->paymask));
+      $self->set('paycardtype', cardtype($self->paymask));
     } else {
-      $self->set('cardtype', '');
+      $self->set('paycardtype', '');
     }
     if ( $self->is_encrypted($self->payinfo) ) {
       #something better?  all it would cause is a decryption error anyway?
@@ -320,8 +322,8 @@ sub payinfo_used {
 
 =item upgrade_set_cardtype
 
-Find all records with a credit card payment type and no cardtype, and
-replace them in order to set their cardtype.
+Find all records with a credit card payment type and no paycardtype, and
+replace them in order to set their paycardtype.
 
 =cut
 
@@ -332,7 +334,7 @@ sub upgrade_set_cardtype {
   local $ignore_masked_payinfo = 1;
   my $search = FS::Cursor->new({
     table     => $class->table,
-    extra_sql => q[ WHERE payby IN('CARD','DCRD') AND cardtype IS NULL ],
+    extra_sql => q[ WHERE payby IN('CARD','DCRD') AND paycardtype IS NULL ],
   });
   while (my $record = $search->fetch) {
     my $error = $record->replace;

commit 050e311ccb5ef9747a87632842081b99453aba4b
Author: Mark Wells <mark at freeside.biz>
Date:   Thu Jul 14 23:32:19 2016 -0700

    store credit card type in cust_payby and transaction records, #71291, schema support

diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm
index 45ff037..f3fca2e 100644
--- a/FS/FS/Schema.pm
+++ b/FS/FS/Schema.pm
@@ -1755,6 +1755,7 @@ sub tables_hashref {
                                                  # index into payby table
                                                  # eventually
         'payinfo',  'varchar',   'NULL', 512, '', '', #see cust_main above
+        'cardtype',    'varchar', 'NULL',   $char_d, '', '',
         'paymask', 'varchar', 'NULL', $char_d, '', '', 
         'paydate',  'varchar', 'NULL', 10, '', '', 
         'paybatch', 'varchar',   'NULL', $char_d, '', '', #for auditing purposes.
@@ -1793,7 +1794,8 @@ sub tables_hashref {
                                                   # index into payby table
                                                   # eventually
         'payinfo',   'varchar',   'NULL', 512, '', '', #see cust_main above
-	'paymask', 'varchar', 'NULL', $char_d, '', '', 
+        'cardtype',    'varchar', 'NULL',   $char_d, '', '',
+        'paymask', 'varchar', 'NULL', $char_d, '', '', 
         #'paydate' ?
         'paybatch',  'varchar',   'NULL', $char_d, '', '', #for auditing purposes.
         'closed',    'char', 'NULL', 1, '', '', 
@@ -2144,7 +2146,8 @@ sub tables_hashref {
                                                      # be index into payby
                                                      # table eventually
         'payinfo',      'varchar',   'NULL', 512, '', '', #see cust_main above
-	'paymask', 'varchar', 'NULL', $char_d, '', '', 
+        'cardtype',    'varchar', 'NULL',   $char_d, '', '',
+        'paymask', 'varchar', 'NULL', $char_d, '', '', 
         'paybatch',     'varchar',   'NULL', $char_d, '', '', 
         'closed',    'char', 'NULL', 1, '', '', 
         'source_paynum', 'int', 'NULL', '', '', '', # link to cust_payby, to prevent unapply of gateway-generated refunds
diff --git a/FS/FS/Upgrade.pm b/FS/FS/Upgrade.pm
index 377a0b1..26cd42a 100644
--- a/FS/FS/Upgrade.pm
+++ b/FS/FS/Upgrade.pm
@@ -391,6 +391,9 @@ sub upgrade_data {
     'cust_refund' => [],
     'banned_pay' => [],
 
+    #cardtype
+    'cust_payby' => [],
+
     #default namespace
     'payment_gateway' => [],
 
diff --git a/FS/FS/cust_pay.pm b/FS/FS/cust_pay.pm
index 587454e..6676317 100644
--- a/FS/FS/cust_pay.pm
+++ b/FS/FS/cust_pay.pm
@@ -96,6 +96,10 @@ Payment Type (See L<FS::payinfo_Mixin> for valid values)
 
 Payment Information (See L<FS::payinfo_Mixin> for data format)
 
+=item cardtype
+
+Credit card type, if appropriate; autodetected.
+
 =item paymask
 
 Masked payinfo (See L<FS::payinfo_Mixin> for how this works)
@@ -1242,6 +1246,12 @@ sub _upgrade_data {  #class method
       process_upgrade_paybatch();
     }
   }
+
+  ###
+  # set cardtype
+  ###
+  $class->upgrade_set_cardtype;
+
 }
 
 sub process_upgrade_paybatch {
diff --git a/FS/FS/cust_pay_void.pm b/FS/FS/cust_pay_void.pm
index 614a88f..0609ea8 100644
--- a/FS/FS/cust_pay_void.pm
+++ b/FS/FS/cust_pay_void.pm
@@ -76,6 +76,10 @@ Payment Type (See L<FS::payinfo_Mixin> for valid values)
 
 card number, check #, or comp issuer (4-8 lowercase alphanumerics; think username), respectively
 
+=item cardtype
+
+Credit card type, if appropriate.
+
 =item paybatch
 
 text field for tracking card processing
diff --git a/FS/FS/cust_refund.pm b/FS/FS/cust_refund.pm
index 1791510..17bd13d 100644
--- a/FS/FS/cust_refund.pm
+++ b/FS/FS/cust_refund.pm
@@ -83,6 +83,10 @@ Payment Type (See L<FS::payinfo_Mixin> for valid payby values)
 
 Payment Information (See L<FS::payinfo_Mixin> for data format)
 
+=item cardtype
+
+Detected credit card type, if appropriate; autodetected.
+
 =item paymask
 
 Masked payinfo (See L<FS::payinfo_Mixin> for how this works)
@@ -516,6 +520,9 @@ sub _upgrade_data {  # class method
   my ($class, %opts) = @_;
   $class->_upgrade_reasonnum(%opts);
   $class->_upgrade_otaker(%opts);
+
+  local $ignore_empty_reasonnum = 1;
+  $class->upgrade_set_cardtype;
 }
 
 =back
diff --git a/FS/FS/payinfo_Mixin.pm b/FS/FS/payinfo_Mixin.pm
index 023aacd..81e04a4 100644
--- a/FS/FS/payinfo_Mixin.pm
+++ b/FS/FS/payinfo_Mixin.pm
@@ -4,6 +4,9 @@ use strict;
 use Business::CreditCard;
 use FS::payby;
 use FS::Record qw(qsearch);
+use FS::UID qw(driver_name);
+use FS::Cursor;
+use Time::Local qw(timelocal);
 
 use vars qw($ignore_masked_payinfo);
 
@@ -196,6 +199,8 @@ sub payinfo_check {
 
   if ( $self->payby eq 'CARD' && ! $self->is_encrypted($self->payinfo) ) {
     my $payinfo = $self->payinfo;
+    my $cardtype = cardtype($payinfo);
+    $self->set('cardtype', $cardtype);
     if ( $ignore_masked_payinfo and $self->mask_payinfo eq $self->payinfo ) {
       # allow it
     } else {
@@ -207,12 +212,18 @@ sub payinfo_check {
         $self->payinfo($1);
         validate($self->payinfo) or return "Illegal credit card number";
         return "Unknown card type" if $self->payinfo !~ /^99\d{14}$/ #token
-                                   && cardtype($self->payinfo) eq "Unknown";
+                                   && $cardtype eq "Unknown";
       } else {
         $self->payinfo('N/A'); #???
       }
     }
   } else {
+    if ( $self->payby eq 'CARD' and $self->paymask ) {
+      # if we can't decrypt the card, at least detect the cardtype
+      $self->set('cardtype', cardtype($self->paymask));
+    } else {
+      $self->set('cardtype', '');
+    }
     if ( $self->is_encrypted($self->payinfo) ) {
       #something better?  all it would cause is a decryption error anyway?
       my $error = $self->ut_anything('payinfo');
@@ -307,6 +318,28 @@ sub payinfo_used {
   return 0;
 }
 
+=item upgrade_set_cardtype
+
+Find all records with a credit card payment type and no cardtype, and
+replace them in order to set their cardtype.
+
+=cut
+
+sub upgrade_set_cardtype {
+  my $class = shift;
+  # assign cardtypes to CARD/DCRDs that need them; check_payinfo_cardtype
+  # will do this. ignore any problems with the cards.
+  local $ignore_masked_payinfo = 1;
+  my $search = FS::Cursor->new({
+    table     => $class->table,
+    extra_sql => q[ WHERE payby IN('CARD','DCRD') AND cardtype IS NULL ],
+  });
+  while (my $record = $search->fetch) {
+    my $error = $record->replace;
+    die $error if $error;
+  }
+}
+
 =back
 
 =head1 BUGS

-----------------------------------------------------------------------

Summary of changes:
 FS/FS/Conf.pm                                      |    7 ++
 FS/FS/Record.pm                                    |   16 +++++
 FS/FS/Schema.pm                                    |    8 ++-
 FS/FS/Upgrade.pm                                   |    6 +-
 FS/FS/cust_location.pm                             |   41 ++++++++++-
 FS/FS/cust_main.pm                                 |   27 +++++--
 FS/FS/cust_main/Billing.pm                         |    6 +-
 FS/FS/cust_main_county.pm                          |   46 ++++++++++++
 FS/FS/cust_pay.pm                                  |   10 +++
 FS/FS/cust_pay_void.pm                             |    4 ++
 FS/FS/cust_refund.pm                               |    9 +++
 FS/FS/payinfo_Mixin.pm                             |   39 ++++++++++-
 FS/FS/svc_Common.pm                                |    8 ++-
 FS/bin/freeside-cdrrewrited                        |    7 ++
 .../search/report_sqlradius_usage-custnum.html     |   71 +++++++++++++++++++
 httemplate/search/report_sqlradius_usage.html      |    1 +
 httemplate/search/sqlradius_usage.html             |   74 +++++++++++++++++---
 httemplate/view/cust_main/packages.html            |   11 +++
 18 files changed, 366 insertions(+), 25 deletions(-)
 create mode 100644 httemplate/search/report_sqlradius_usage-custnum.html




More information about the freeside-commits mailing list