Merge branch 'master' of git.freeside.biz:/home/git/freeside
authorIvan Kohler <ivan@freeside.biz>
Tue, 1 Nov 2016 23:20:44 +0000 (16:20 -0700)
committerIvan Kohler <ivan@freeside.biz>
Tue, 1 Nov 2016 23:20:44 +0000 (16:20 -0700)
15 files changed:
FS/FS/ClientAPI/MyAccount.pm
FS/FS/cust_main.pm
FS/FS/cust_main/Billing_Realtime.pm
FS/FS/cust_main_county.pm
FS/FS/cust_payby.pm
FS/FS/geocode_Mixin.pm
FS/FS/part_pkg.pm
FS/FS/part_pkg/global_Mixin.pm
FS/FS/part_pkg/voip_cdr.pm
FS/FS/part_pkg/voip_inbound.pm
FS/FS/payinfo_Mixin.pm
bin/wa_tax_rate_update
httemplate/browse/cust_main_county.cgi
httemplate/misc/process/payment.cgi
httemplate/search/sqlradius_usage.html

index 7c17ae3..091d6ac 100644 (file)
@@ -1022,7 +1022,7 @@ sub validate_payment {
     validate($payinfo)
       or return { 'error' => gettext('invalid_card') }; # . ": ". $self->payinfo
     return { 'error' => gettext('unknown_card_type') }
-      if $payinfo !~ /^99\d{14}$/ && cardtype($payinfo) eq "Unknown";
+      if !$cust_main->tokenized($payinfo) && cardtype($payinfo) eq "Unknown";
 
     if ( length($p->{'paycvv'}) && $p->{'paycvv'} !~ /^\s*$/ ) {
       if ( cardtype($payinfo) eq 'American Express card' ) {
index 9c8e374..2136ad2 100644 (file)
@@ -1895,7 +1895,7 @@ sub check_payinfo_cardtype {
   my $payinfo = $self->payinfo;
   $payinfo =~ s/\D//g;
 
-  return '' if $payinfo =~ /^99\d{14}$/; #token
+  return '' if $self->tokenized($payinfo); #token
 
   my %bop_card_types = map { $_=>1 } values %{ card_types() };
   my $cardtype = cardtype($payinfo);
index 0b6b099..7718f7a 100644 (file)
@@ -376,14 +376,18 @@ sub _bop_content {
 }
 
 sub _tokenize_card {
-  my ($self,$transaction,$cust_payby,$log,%opt) = @_;
+  my ($self,$transaction,$options,$log,%opt) = @_;
+  # options is for entire process, so we can update payinfo
+  # opt is just for this call, only key is replace
 
+  my $cust_payby = $options->{'cust_payby'};
   if ( $cust_payby
        and $transaction->can('card_token') 
        and $transaction->card_token 
-       and $cust_payby->payinfo !~ /^99\d{14}$/ #not already tokenized
+       and !$cust_payby->tokenized #not already tokenized
   ) {
 
+    $options->{'payinfo'} = $transaction->card_token;
     $cust_payby->payinfo($transaction->card_token);
 
     my $error;
@@ -400,6 +404,18 @@ sub _tokenize_card {
 
 }
 
+# only store payinfo in cust_pay/cust_pay_pending
+# if it's a tokenized card or if processor requires card for void
+sub _cust_pay_opts {
+  my ($self,$payby,$payinfo,$transaction) = @_;
+  ( (($payby eq 'CARD') && $self->tokenized($payinfo))
+    || (($payby eq 'CARD') && $transaction->info('CC_void_requires_card'))
+    || (($payby eq 'CHEK') && $transaction->info('ECHECK_void_requires_account'))
+  )
+    ? ('payinfo' => $payinfo)
+    : ();
+}
+
 my %bop_method2payby = (
   'CC'     => 'CARD',
   'ECHECK' => 'CHEK',
@@ -665,12 +681,15 @@ sub realtime_bop {
 
   #okay, good to go, if we're a duplicate, cust_pay_pending will kick us out
 
+  my $transaction = new $namespace( $payment_gateway->gateway_module,
+                                    $self->_bop_options(\%options),
+                                  );
+
   my $cust_pay_pending = new FS::cust_pay_pending {
     'custnum'           => $self->custnum,
     'paid'              => $options{amount},
     '_date'             => '',
     'payby'             => $bop_method2payby{$options{method}},
-    'payinfo'           => $options{payinfo},
     'paymask'           => $options{paymask},
     'paydate'           => $paydate,
     'recurring_billing' => $content{recurring_billing},
@@ -679,6 +698,7 @@ sub realtime_bop {
     'gatewaynum'        => $payment_gateway->gatewaynum || '',
     'session_id'        => $options{session_id} || '',
     'jobnum'            => $options{depend_jobnum} || '',
+    $self->_cust_pay_opts($options{payinfo},$transaction),
   };
   $cust_pay_pending->payunique( $options{payunique} )
     if defined($options{payunique}) && length($options{payunique});
@@ -695,10 +715,6 @@ sub realtime_bop {
   my( $action1, $action2 ) =
     split( /\s*\,\s*/, $payment_gateway->gateway_action );
 
-  my $transaction = new $namespace( $payment_gateway->gateway_module,
-                                    $self->_bop_options(\%options),
-                                  );
-
   $transaction->content(
     'type'           => $options{method},
     $self->_bop_auth(\%options),          
@@ -811,7 +827,7 @@ sub realtime_bop {
   # Tokenize
   ###
 
-  my $error = $self->_tokenize_card($transaction,$options{'cust_payby'},$log,'replace' => 1);
+  my $error = $self->_tokenize_card($transaction,\%options,$log,'replace' => 1);
   return $error if $error;
 
   ###
@@ -849,9 +865,7 @@ sub fake_bop {
      'paid'     => $options{amount},
      '_date'    => '',
      'payby'    => $bop_method2payby{$options{method}},
-     #'payinfo'  => $payinfo,
      'payinfo'  => '4111111111111111',
-     #'paydate'  => $paydate,
      'paydate'  => '2012-05-01',
      'processor'      => 'FakeProcessor',
      'auth'           => '54',
@@ -925,7 +939,6 @@ sub _realtime_bop_result {
        'paid'     => $cust_pay_pending->paid,
        '_date'    => '',
        'payby'    => $cust_pay_pending->payby,
-       'payinfo'  => $options{'payinfo'},
        'paymask'  => $options{'paymask'} || $cust_pay_pending->paymask,
        'paydate'  => $cust_pay_pending->paydate,
        'pkgnum'   => $cust_pay_pending->pkgnum,
@@ -935,6 +948,7 @@ sub _realtime_bop_result {
        'auth'           => $transaction->authorization,
        'order_number'   => $order_number || '',
        'no_auto_apply'  => $options{'no_auto_apply'} ? 'Y' : '',
+       $self->_cust_pay_opts($options{payinfo},$transaction),
     } );
     #doesn't hurt to know, even though the dup check is in cust_pay_pending now
     $cust_pay->payunique( $options{payunique} )
@@ -1840,7 +1854,9 @@ sub realtime_verify_bop {
   ###
 
   my $error;
-  my $transaction; #need this back so we can do _tokenize_card
+  my $transaction = new $namespace( $payment_gateway->gateway_module,
+                                    $self->_bop_options(\%options),
+                                  ); #need this back so we can do _tokenize_card
 
   # don't mutex the customer here, because they might be uncommitted. and
   # this is only verification. it doesn't matter if they have other
@@ -1851,13 +1867,13 @@ sub realtime_verify_bop {
     'paid'              => '1.00',
     '_date'             => '',
     'payby'             => $bop_method2payby{'CC'},
-    'payinfo'           => $options{payinfo},
     'paymask'           => $options{paymask},
     'paydate'           => $paydate,
     'pkgnum'            => $options{'pkgnum'},
     'status'            => 'new',
     'gatewaynum'        => $payment_gateway->gatewaynum || '',
     'session_id'        => $options{session_id} || '',
+    $self->_cust_pay_opts($options{payinfo},$transaction),
   };
   $cust_pay_pending->payunique( $options{payunique} )
     if defined($options{payunique}) && length($options{payunique});
@@ -1888,10 +1904,6 @@ sub realtime_verify_bop {
       if $DEBUG > 1;
     warn Dumper($cust_pay_pending) if $DEBUG > 2;
 
-    $transaction = new $namespace( $payment_gateway->gateway_module,
-                                   $self->_bop_options(\%options),
-                                    );
-
     $transaction->content(
       'type'           => 'CC',
       $self->_bop_auth(\%options),          
@@ -2115,7 +2127,7 @@ sub realtime_verify_bop {
 
   #important that we not pass replace option here,
   #because cust_payby->replace uses realtime_verify_bop!
-  $self->_tokenize_card($transaction,$options{'cust_payby'},$log);
+  $self->_tokenize_card($transaction,\%options,$log);
 
   ###
   # result handling
@@ -2162,7 +2174,7 @@ sub realtime_tokenize {
   return "No cust_payby" unless $options{'cust_payby'};
   $self->_bop_cust_payby_options(\%options);
   return '' unless $options{method} eq 'CC';
-  return '' if $options{payinfo} =~ /^99\d{14}$/; #already tokenized
+  return '' if $self->tokenized($options{payinfo}); #already tokenized
 
   ###
   # select a gateway
@@ -2254,7 +2266,7 @@ sub realtime_tokenize {
 
     #important that we not pass replace option here, 
     #because cust_payby->replace uses realtime_tokenize!
-    $self->_tokenize_card($transaction,$options{'cust_payby'},$log);
+    $self->_tokenize_card($transaction,\%options,$log);
 
   } else {
 
@@ -2266,6 +2278,12 @@ sub realtime_tokenize {
 
 }
 
+sub tokenized {
+  my $this = shift;
+  my $payinfo = shift;
+  $payinfo =~ /^99\d{14}$/;
+}
+
 =back
 
 =head1 BUGS
index a1233d0..65fb722 100644 (file)
@@ -682,6 +682,37 @@ END
 
 }
 
+sub _merge_into {
+  # for internal use: takes another cust_main_county object, transfers
+  # all existing references to this record to that one, and deletes this
+  # one.
+  my $record = shift;
+  my $other = shift or die "record to merge into must be provided";
+  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;
+  }
+}
+
 sub _upgrade_data {
   my $class = shift;
   # assume taxes in Washington with district numbers, and null name, or 
@@ -704,6 +735,28 @@ sub _upgrade_data {
     }
     FS::upgrade_journal->set_done($journal);
   }
+  my @key_fields = (qw(city county state country district taxname taxclass));
+
+  # remove duplicates (except disabled records)
+  my @duplicate_sets = qsearch({
+    table => 'cust_main_county',
+    select => FS::Record::group_concat_sql('taxnum', ',') . ' AS taxnums, ' .
+              join(',', @key_fields),
+    extra_sql => ' WHERE tax > 0
+      GROUP BY city, county, state, country, district, taxname, taxclass
+      HAVING COUNT(*) > 1'
+  });
+  warn "Found ".scalar(@duplicate_sets)." set(s) of duplicate tax definitions\n"
+    if @duplicate_sets;
+  foreach my $set (@duplicate_sets) {
+    my @taxnums = split(',', $set->get('taxnums'));
+    my $first = FS::cust_main_county->by_key(shift @taxnums);
+    foreach my $taxnum (@taxnums) {
+      my $record = FS::cust_main_county->by_key($taxnum);
+      $record->_merge_into($first);
+    }
+  }
+
   # trim whitespace and convert to uppercase in the 'city' field.
   foreach my $record (qsearch({
     table => 'cust_main_county',
@@ -714,33 +767,10 @@ sub _upgrade_data {
     # 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 %match = map { $_ => $record->get($_) } @key_fields;
     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;
-      }
+      $record->_merge_into($other);
     } else {
       # else there is no record this one duplicates, so just fix it
       my $error = $record->replace;
index 626fc9f..53608cf 100644 (file)
@@ -276,7 +276,7 @@ sub replace {
 
   if ( $self->payby =~ /^(CARD|CHEK)$/
        && ( ( $self->get('payinfo') ne $old->get('payinfo')
-              && $self->get('payinfo') !~ /^99\d{14}$/ 
+              && !$self->tokenized 
             )
             || grep { $self->get($_) ne $old->get($_) } qw(paydate payname)
           )
@@ -357,7 +357,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->tokenized; #token
     
     return gettext('unknown_card_type') if $cardtype eq "Unknown";
     
@@ -546,7 +546,7 @@ sub check_payinfo_cardtype {
   my $payinfo = $self->payinfo;
   $payinfo =~ s/\D//g;
 
-  if ( $payinfo =~ /^99\d{14}$/ ) {
+  if ( $self->tokenized($payinfo) ) {
     $self->set('paycardtype', 'Tokenized');
     return '';
   }
index a372faa..09b1131 100644 (file)
@@ -273,7 +273,7 @@ sub process_district_update {
     my $error = $self->replace;
     die $error if $error;
 
-    my %hash = map { $_ => $tax_info->{$_} } 
+    my %hash = map { $_ => uc( $tax_info->{$_} ) } 
       qw( district city county state country );
     $hash{'source'} = $method; # apply the update only to taxes we maintain
 
index 008ba8a..35f178e 100644 (file)
@@ -1917,13 +1917,27 @@ sub calc_remain { 0; }
 =item calc_units CUST_PKG
 
 This returns the number of provisioned svc_phone records, or, of the package
-count_available_phones option is set, the number available to be provisoined
+count_available_phones option is set, the number available to be provisioned
 in the package.
 
 =cut
 
-#fallback that returns 0 for old legacy packages with no plan
-sub calc_units  { 0; }
+sub calc_units {
+  my($self, $cust_pkg ) = @_;
+  my $count = 0;
+  if ( $self->option('count_available_phones', 1)) {
+    foreach my $pkg_svc ($cust_pkg->part_pkg->pkg_svc) {
+      if ($pkg_svc->part_svc->svcdb eq 'svc_phone') { # svc_pbx?
+        $count += $pkg_svc->quantity || 0;
+      }
+    }
+    $count *= $cust_pkg->quantity;
+  } else {
+    $count =
+      scalar(grep { $_->part_svc->svcdb eq 'svc_phone' } $cust_pkg->cust_svc);
+  }
+  $count;
+}
 
 #fallback for everything not based on flat.pm
 sub recur_temporality { 'upcoming'; }
index e82602e..59eaaaa 100644 (file)
@@ -30,6 +30,10 @@ sub validate_moneyn {
   return '';
 }
 
+tie my %count_available_phones, 'Tie::IxHash', (
+  0 => 'Provisioned phone services',
+  1 => 'All available phone services',
+);
 
 %info = (
   'disabled' => 1,
@@ -63,6 +67,11 @@ sub validate_moneyn {
       'name' => 'Automatic suspension period before cancelling (configuration setting part_pkg-delay_cancel-days)',
       'type' => 'checkbox',
     },
+    'count_available_phones' => { 'name' => 'Count taxable phone lines',
+      'type' => 'radio',
+      'options' => \%count_available_phones,
+      'default' => 0,
+    },
 
     # miscellany--maybe put this in a separate module?
 
@@ -134,6 +143,8 @@ sub validate_moneyn {
     unused_credit_change
     delay_cancel
 
+    count_available_phones
+
     a2billing_tariff
     a2billing_type
     a2billing_simultaccess
index 420026d..9ecdba6 100644 (file)
@@ -289,10 +289,6 @@ tie my %accountcode_tollfree_field, 'Tie::IxHash',
                                'type' => 'checkbox',
                              },
 
-    'count_available_phones' => { 'name' => 'Consider for tax purposes the number of lines to be svc_phones that may be provisioned rather than those that actually are.',
-                           'type' => 'checkbox',
-                         },
-
     #XXX also have option for an external db?  these days we suck them into ours
 #    'cdr_location' => { 'name' => 'CDR database location'
 #                        'type' => 'select',
@@ -353,7 +349,7 @@ tie my %accountcode_tollfree_field, 'Tie::IxHash',
                        usage_mandate usage_section summarize_usage 
                        usage_showzero bill_every_call bill_inactive_svcs
                        bill_only_pkg_dates
-                       count_available_phones suspend_bill 
+                       suspend_bill 
                      )
                   ],
   'weight' => 41,
@@ -656,25 +652,6 @@ sub is_free {
   0;
 }
 
-#  This equates svc_phone records; perhaps svc_phone should have a field
-#  to indicate it represents a line
-sub calc_units {    
-  my($self, $cust_pkg ) = @_;
-  my $count = 0;
-  if ( $self->option('count_available_phones', 1)) {
-    foreach my $pkg_svc ($cust_pkg->part_pkg->pkg_svc) {
-      if ($pkg_svc->part_svc->svcdb eq 'svc_phone') { # svc_pbx?
-        $count += $pkg_svc->quantity || 0;
-      }
-    }
-    $count *= $cust_pkg->quantity;
-  } else {
-    $count = 
-      scalar(grep { $_->part_svc->svcdb eq 'svc_phone' } $cust_pkg->cust_svc);
-  }
-  $count;
-}
-
 sub reset_usage {
   my ($self, $cust_pkg, %opt) = @_;
   my @part_pkg_usage = $self->part_pkg_usage or return '';
index e911439..15af706 100644 (file)
@@ -399,15 +399,5 @@ sub is_free {
   0;
 }
 
-#  This equates svc_phone records; perhaps svc_phone should have a field
-#  to indicate it represents a line
-#  #XXX no count_available_phones?
-sub calc_units {    
-  my($self, $cust_pkg ) = @_;
-  my $count = 
-      scalar(grep { $_->part_svc->svcdb eq 'svc_phone' } $cust_pkg->cust_svc);
-  $count;
-}
-
 1;
 
index 3a32ad5..a0a2cbc 100644 (file)
@@ -67,9 +67,9 @@ sub payinfo {
   my($self,$payinfo) = @_;
 
   if ( defined($payinfo) ) {
-    $self->paymask($self->mask_payinfo) unless $self->payinfo =~ /^99\d{14}$/; #make sure old mask is set
+    $self->paymask($self->mask_payinfo) unless $self->tokenized; #make sure old mask is set
     $self->setfield('payinfo', $payinfo);
-    $self->paymask($self->mask_payinfo) unless $payinfo =~ /^99\d{14}$/; #remask unless tokenizing
+    $self->paymask($self->mask_payinfo) unless $self->tokenized($payinfo); #remask unless tokenizing
   } else {
     $self->getfield('payinfo');
   }
@@ -130,7 +130,7 @@ sub mask_payinfo {
   # Check to see if it's encrypted...
   if ( ref($self) && $self->is_encrypted($payinfo) ) {
     return 'N/A';
-  } elsif ( $payinfo =~ /^99\d{14}$/ || $payinfo eq 'N/A' ) { #token
+  } elsif ( $self->tokenized($payinfo) || $payinfo eq 'N/A' ) { #token
     return 'N/A (tokenized)'; #?
   } else { # if not, mask it...
 
@@ -198,7 +198,7 @@ sub payinfo_check {
 
     my $payinfo = $self->payinfo;
     my $cardtype = cardtype($payinfo);
-    $cardtype = 'Tokenized' if $payinfo =~ /^99\d{14}$/;
+    $cardtype = 'Tokenized' if $self->tokenized;
     $self->set('paycardtype', $cardtype);
 
     if ( $ignore_masked_payinfo and $self->mask_payinfo eq $self->payinfo ) {
@@ -233,6 +233,7 @@ sub payinfo_check {
     }
   }
 
+  return '';
 }
 
 =item payby_payinfo_pretty [ LOCALE ]
@@ -453,6 +454,15 @@ sub process_set_cardtype {
   }
 }
 
+sub tokenized {
+  my $self = shift;
+  my $payinfo = scalar(@_) ? shift : $self->payinfo;
+  ## or just $self->cust_main->tokenized($payinfo) ??
+  ##   everything that currently uses this mixin is linked to cust_main,
+  ##   but just in case, false laziness w/ FS::cust_main::Billing_Realtime
+  $payinfo =~ /^99\d{14}$/;
+}
+
 =back
 
 =head1 BUGS
index 2d493db..fbca9dd 100644 (file)
@@ -78,6 +78,7 @@ my $total_skipped = 0;
 while ( !$csv->eof ) {
   my $line = $csv->getline_hr($fh);
   my $district = $line->{Code} or next;
+  $district = sprintf('%04d', $district);
   my $tax = sprintf('%.1f', $line->{Rate} * 100);
   my $changed = 0;
   my $skipped = 0;
index 5226148..26a3e21 100755 (executable)
@@ -467,13 +467,13 @@ my @fields = (
                              ? '&nbsp;'. add_link(
                                  desc => 'Add more counties',
                                  col  => 'state',
-                                 label=> 'add&nbsp;more&nbsp;counties',
+                                 label=> 'add more counties',
                                  row  => $_[0],
                                  cgi  => $cgi,
                                ).
                                ' '. collapse_link(
                                  col  => 'state',
-                                 label=> 'remove&nbsp;all&nbsp;counties',
+                                 label=> 'remove all counties',
                                  row  => $_[0],
                                  cgi  => $cgi,
                                )
@@ -484,7 +484,7 @@ my @fields = (
               ? ''
               : '&nbsp;'. expand_link( desc  => 'Add States',
                                        row   => $_[0],
-                                       label => 'add&nbsp;states',
+                                       label => 'add states',
                                        cgi  => $cgi,
                                      )
           );
@@ -503,18 +503,18 @@ my @fields = (
                        ? '&nbsp;'. add_link(
                            desc => 'Add more cities',
                            col  => 'county',
-                           label=> 'add&nbsp;more&nbsp;cities',
+                           label=> 'add more cities',
                            row  => $_[0],
                            cgi  => $cgi,
                          ).
                          ' '. collapse_link(
                            col  => 'county',
-                           label=> 'remove&nbsp;all&nbsp;cities',
+                           label=> 'remove all cities',
                            row  => $_[0],
                            cgi  => $cgi,
                          )
                        : '&nbsp;'. remove_link( col  => 'county',
-                                                label=> 'remove&nbsp;county',
+                                                label=> 'remove county',
                                                 row  => $_[0],
                                                 cgi  => $cgi,
                                               );
@@ -525,7 +525,7 @@ my @fields = (
           : '(all)&nbsp;'.
               expand_link(   desc  => 'Add Counties',
                              row   => $_[0],
-                             label => 'add&nbsp;counties',
+                             label => 'add counties',
                              cgi  => $cgi,
                          );
       },
@@ -541,7 +541,7 @@ my @fields = (
           } else {
             $r->city. '&nbsp;'.
               remove_link( col  => 'city',
-                           label=> 'remove&nbsp;city',
+                           label=> 'remove city',
                            row  => $r,
                            cgi  => $cgi,
                          );
@@ -550,7 +550,7 @@ my @fields = (
           '(all)&nbsp;'.
             expand_link(   desc  => 'Add Cities',
                            row   => $r,
-                           label => 'add&nbsp;cities',
+                           label => 'add cities',
                            cgi  => $cgi,
                        );
         }
@@ -562,7 +562,7 @@ my @fields = (
         if ( $r->district ) {
           $r->district . '&nbsp;'.
             remove_link( col  => 'district',
-                         label=> 'remove&nbsp;district',
+                         label=> 'remove district',
                          row  => $r,
                          cgi  => $cgi,
                        );
index 74ca734..84687f0 100644 (file)
@@ -135,7 +135,7 @@ if ( (my $custpaybynum = scalar($cgi->param('custpaybynum'))) > 0 ) {
     validate($payinfo)
       or errorpage(gettext('invalid_card'));
 
-    unless ( $payinfo =~ /^99\d{14}$/ ) { #token
+    unless ( $cust_main->tokenized($payinfo) ) { #token
 
       my $cardtype = cardtype($payinfo);
 
index 846b50c..b810a8b 100644 (file)
@@ -5,8 +5,8 @@
 % $cgi->param('agentnum', $agent->agentnum); #for download links
 <DIV WIDTH="100%" STYLE="page-break-after: always">
 <FONT SIZE=6><% $agent->agent %></FONT><BR><BR>
-  <& cust_pkg_sqlradius_usage.html, 
-      exports           => @exports,
+  <& sqlradius_usage.html, 
+      exports           => \@exports,
       agentnum          => $agent->agentnum,
       nohtmlheader      => 1,
       download_label    => 'Download this section',
@@ -154,6 +154,17 @@ my %usage_param = (
 
 my @total_usage = ('', 0, 0, 0); # username, input, output, input + output
 
+# remember which exports apply to which services, so we don't inappropriately
+# ask the wrong ones for usage stats
+my %export_svcparts;
+foreach my $export (@exports) {
+  my %seen;
+  foreach ($export->export_svc) {
+    $seen{ $_->svcpart } = 1;
+  }
+  $export_svcparts{ $export->exportnum } = \%seen;
+}
+
 # a single sub to collect data for each package, aggregated across both
 # services and exports.  when we add per-service breakdown, this should also
 # keep the per-service data, but not needed yet
@@ -163,16 +174,20 @@ my $cust_pkg_stats_sub = sub {
     my ($upbytes, $downbytes, $totalbytes) = (0, 0, 0);
     my $display_username;
     foreach my $svcnum ( split(',', $cust_pkg->get('svcnums_concat')) ) {
+      my $cust_svc = FS::cust_svc->by_key($svcnum);
+      my $svc = $cust_svc->svc_x;
       foreach my $export (@exports) {
-        my $svc = FS::cust_svc->by_key($svcnum)->svc_x;
-        my $username = $export->export_username($svc);
-        my $usage = $export->usage_sessions({ %usage_param, 'svc' => $svc });
-        # returns arrayref with one row
-        $upbytes += $usage->[0]->{'acctinputoctets'};
-        $downbytes += $usage->[0]->{'acctoutputoctets'};
-        # in combined services mode with multiple users/MAC addresses per
-        # package, this will just show one of them arbitrarily.
-        $display_username ||= $username;
+        if ( $export_svcparts{ $export->exportnum }{ $cust_svc->svcpart } ) {
+          my $username = $export->export_username($svc);
+          my $usage = $export->usage_sessions({ %usage_param, 'svc' => $svc });
+          # returns arrayref with one row
+          $upbytes += $usage->[0]->{'acctinputoctets'};
+          $downbytes += $usage->[0]->{'acctoutputoctets'};
+          # in combined services mode with multiple users/MAC addresses per
+          # package, this will just show one of them arbitrarily.
+          $display_username ||= $username;
+        }
+        # else this export doesn't apply so skip it
       }
     }
     $total_usage[1] += $upbytes;