#30616 Fix AU toll-free
[freeside.git] / FS / FS / cdr.pm
index fdec921..9859dfa 100644 (file)
@@ -11,6 +11,7 @@ use Date::Parse;
 use Date::Format;
 use Time::Local;
 use List::Util qw( first min );
 use Date::Format;
 use Time::Local;
 use List::Util qw( first min );
+use Text::CSV_XS;
 use FS::UID qw( dbh );
 use FS::Conf;
 use FS::Record qw( qsearch qsearchs );
 use FS::UID qw( dbh );
 use FS::Conf;
 use FS::Record qw( qsearch qsearchs );
@@ -91,6 +92,8 @@ following fields are currently supported:
 
 =item dst_ip_addr - Destination IP address (same)
 
 
 =item dst_ip_addr - Destination IP address (same)
 
+=item dst_term - Terminating destination number (if different from dst)
+
 =item startdate - Start of call (UNIX-style integer timestamp)
 
 =item answerdate - Answer time of call (UNIX-style integer timestamp)
 =item startdate - Start of call (UNIX-style integer timestamp)
 
 =item answerdate - Answer time of call (UNIX-style integer timestamp)
@@ -193,6 +196,7 @@ sub table_info {
         #'lastdata'              => '',
         'src_ip_addr'           => 'Source IP',
         'dst_ip_addr'           => 'Dest. IP',
         #'lastdata'              => '',
         'src_ip_addr'           => 'Source IP',
         'dst_ip_addr'           => 'Dest. IP',
+        'dst_term'              => 'Termination dest.',
         'startdate'             => 'Start date',
         'answerdate'            => 'Answer date',
         'enddate'               => 'End date',
         'startdate'             => 'Start date',
         'answerdate'            => 'Answer date',
         'enddate'               => 'End date',
@@ -325,12 +329,16 @@ sub check {
     $self->billsec(  $self->enddate - $self->answerdate );
   } 
 
     $self->billsec(  $self->enddate - $self->answerdate );
   } 
 
+  if ( ! $self->enddate && $self->startdate && $self->duration ) {
+    $self->enddate( $self->startdate + $self->duration );
+  }
+
   $self->set_charged_party;
 
   #check the foreign keys even?
   #do we want to outright *reject* the CDR?
   my $error =
   $self->set_charged_party;
 
   #check the foreign keys even?
   #do we want to outright *reject* the CDR?
   my $error =
-       $self->ut_numbern('acctid')
+       $self->ut_numbern('acctid');
 
   #add a config option to turn these back on if someone needs 'em
   #
 
   #add a config option to turn these back on if someone needs 'em
   #
@@ -342,7 +350,7 @@ sub check {
   #
   #  # Telstra =1, Optus = 2, RSL COM = 3
   #  || $self->ut_foreign_keyn('carrierid', 'cdr_carrier', 'carrierid' )
   #
   #  # Telstra =1, Optus = 2, RSL COM = 3
   #  || $self->ut_foreign_keyn('carrierid', 'cdr_carrier', 'carrierid' )
-  ;
+
   return $error if $error;
 
   $self->SUPER::check;
   return $error if $error;
 
   $self->SUPER::check;
@@ -360,7 +368,14 @@ to inspect other field.
 sub is_tollfree {
   my $self = shift;
   my $field = scalar(@_) ? shift : 'dst';
 sub is_tollfree {
   my $self = shift;
   my $field = scalar(@_) ? shift : 'dst';
-  ( $self->$field() =~ /^(\+?1)?8(8|([02-7])\3)/ ) ? 1 : 0;
+  my $country = $conf->config('tollfree-country') || '';
+  if ( $country eq 'AU' ) { 
+    ( $self->$field() =~ /^(\+?61)?(1800|1300)/ ) ? 1 : 0;
+  } elsif ( $country eq 'NZ' ) { 
+    ( $self->$field() =~ /^(\+?64)?(800|508)/ ) ? 1 : 0;
+  } else { #NANPA (US/Canaada)
+    ( $self->$field() =~ /^(\+?1)?8(8|([02-7])\3)/ ) ? 1 : 0;
+  }
 }
 
 =item set_charged_party
 }
 
 =item set_charged_party
@@ -421,12 +436,25 @@ sub set_charged_party {
 Sets the status to the provided string.  If there is an error, returns the
 error, otherwise returns false.
 
 Sets the status to the provided string.  If there is an error, returns the
 error, otherwise returns false.
 
+If status is being changed from 'rated' to some other status, also removes
+any usage allocations to this CDR.
+
 =cut
 
 sub set_status {
   my($self, $status) = @_;
 =cut
 
 sub set_status {
   my($self, $status) = @_;
+  my $old_status = $self->freesidestatus;
   $self->freesidestatus($status);
   $self->freesidestatus($status);
-  $self->replace;
+  my $error = $self->replace;
+  if ( $old_status eq 'rated' and $status ne 'done' ) {
+    # deallocate any usage
+    foreach (qsearch('cdr_cust_pkg_usage', {acctid => $self->acctid})) {
+      my $cust_pkg_usage = $_->cust_pkg_usage;
+      $cust_pkg_usage->set('minutes', $cust_pkg_usage->minutes + $_->minutes);
+      $error ||= $cust_pkg_usage->replace || $_->delete;
+    }
+  }
+  $error;
 }
 
 =item set_status_and_rated_price STATUS RATED_PRICE [ SVCNUM [ OPTION => VALUE ... ] ]
 }
 
 =item set_status_and_rated_price STATUS RATED_PRICE [ SVCNUM [ OPTION => VALUE ... ] ]
@@ -556,25 +584,19 @@ sub parse_number {
 
 Rates this CDR according and sets the status to 'rated'.
 
 
 Rates this CDR according and sets the status to 'rated'.
 
-Available options are: part_pkg, svcnum, single_price_included_minutes, region_group, region_group_included_minutes.
+Available options are: part_pkg, svcnum, plan_included_min,
+detail_included_min_hashref.
 
 part_pkg is required.
 
 If svcnum is specified, will also associate this CDR with the specified svcnum.
 
 
 part_pkg is required.
 
 If svcnum is specified, will also associate this CDR with the specified svcnum.
 
-single_price_included_minutes is requried for single_price price plans
-(otherwise unused/ignored).  It should be set to a scalar reference of the
-number of included minutes and will be decremented by the rated minutes of this
+plan_included_min should be set to a scalar reference of the number of 
+included minutes and will be decremented by the rated minutes of this
 CDR.
 
 CDR.
 
-region_group_included_minutes is required for prefix price plans which have
-included minutes (otherwise unused/ignored).  It should be set to a scalar
-reference of the number of included minutes and will be decremented by the
-rated minutes of this CDR.
-
-region_group_included_minutes_hashref is required for prefix price plans which
-have included minues (otehrwise unused/ignored).  It should be set to an empty
-hashref at the start of a month's rating and then preserved across CDRs.
+detail_included_min_hashref should be set to an empty hashref at the 
+start of a month's rating and then preserved across CDRs.
 
 =cut
 
 
 =cut
 
@@ -598,6 +620,7 @@ our %interval_cache = (); # for timed rates
 sub rate_prefix {
   my( $self, %opt ) = @_;
   my $part_pkg = $opt{'part_pkg'} or return "No part_pkg specified";
 sub rate_prefix {
   my( $self, %opt ) = @_;
   my $part_pkg = $opt{'part_pkg'} or return "No part_pkg specified";
+  my $cust_pkg = $opt{'cust_pkg'};
 
   my $da_rewrote = 0;
   # this will result in those CDRs being marked as done... is that 
 
   my $da_rewrote = 0;
   # this will result in those CDRs being marked as done... is that 
@@ -625,14 +648,52 @@ sub rate_prefix {
                                             );
   }
 
                                             );
   }
 
+  if ( $part_pkg->option_cacheable('skip_same_customer')
+      and ! $self->is_tollfree ) {
+    my ($dst_countrycode, $dst_number) = $self->parse_number(
+      column => 'dst',
+      international_prefix => $part_pkg->option_cacheable('international_prefix'),
+      domestic_prefix => $part_pkg->option_cacheable('domestic_prefix'),
+    );
+    my $dst_same_cust = FS::Record->scalar_sql(
+        'SELECT COUNT(svc_phone.svcnum) AS count '.
+        'FROM cust_pkg ' .
+        'JOIN cust_svc   USING (pkgnum) ' .
+        'JOIN svc_phone  USING (svcnum) ' .
+        'WHERE svc_phone.countrycode = ' . dbh->quote($dst_countrycode) .
+        ' AND svc_phone.phonenum = ' . dbh->quote($dst_number) .
+        ' AND cust_pkg.custnum = ' . $cust_pkg->custnum,
+    );
+    if ( $dst_same_cust > 0 ) {
+      warn "not charging for CDR (same source and destination customer)\n" if $DEBUG;
+      return $self->set_status_and_rated_price( 'skipped',
+                                                0,
+                                                $opt{'svcnum'},
+                                              );
+    }
+  }
+
     
     
+
+
   ###
   # look up rate details based on called station id
   # (or calling station id for toll free calls)
   ###
 
   ###
   # look up rate details based on called station id
   # (or calling station id for toll free calls)
   ###
 
+  my $eff_ratenum = $self->is_tollfree('accountcode')
+    ? $part_pkg->option_cacheable('accountcode_tollfree_ratenum')
+    : '';
+
   my( $to_or_from, $column );
   my( $to_or_from, $column );
-  if ( $self->is_tollfree && ! $part_pkg->option_cacheable('disable_tollfree') )
+  if(
+        ( $self->is_tollfree
+           && ! $part_pkg->option_cacheable('disable_tollfree')
+        )
+     or ( $eff_ratenum
+           && $part_pkg->option_cacheable('accountcode_tollfree_field') eq 'src'
+        )
+    )
   { #tollfree call
     $to_or_from = 'from';
     $column = 'src';
   { #tollfree call
     $to_or_from = 'from';
     $column = 'src';
@@ -653,10 +714,6 @@ sub rate_prefix {
   #asterisks here causes inserting the detail to barf, so:
   $pretty_dst =~ s/\*//g;
 
   #asterisks here causes inserting the detail to barf, so:
   $pretty_dst =~ s/\*//g;
 
-  my $eff_ratenum = $self->is_tollfree('accountcode')
-    ? $part_pkg->option_cacheable('accountcode_tollfree_ratenum')
-    : '';
-
   my $ratename = '';
   my $intrastate_ratenum = $part_pkg->option_cacheable('intrastate_ratenum');
   if ( $intrastate_ratenum && !$self->is_tollfree ) {
   my $ratename = '';
   my $intrastate_ratenum = $part_pkg->option_cacheable('intrastate_ratenum');
   if ( $intrastate_ratenum && !$self->is_tollfree ) {
@@ -770,9 +827,10 @@ sub rate_prefix {
   # We don't round _anything_ (except granularizing) 
   # until the final $charge = sprintf("%.2f"...).
 
   # We don't round _anything_ (except granularizing) 
   # until the final $charge = sprintf("%.2f"...).
 
-  my $seconds_left = $part_pkg->option_cacheable('use_duration')
-                       ? $self->duration
-                       : $self->billsec;
+  my $rated_seconds = $part_pkg->option_cacheable('use_duration')
+                        ? $self->duration
+                        : $self->billsec;
+  my $seconds_left = $rated_seconds;
 
   #no, do this later so it respects (group) included minutes
   #  # charge for the first (conn_sec) seconds
 
   #no, do this later so it respects (group) included minutes
   #  # charge for the first (conn_sec) seconds
@@ -780,7 +838,7 @@ sub rate_prefix {
   #  $seconds_left -= $seconds; 
   #  $weektime     += $seconds;
   #  my $charge = $rate_detail->conn_charge; 
   #  $seconds_left -= $seconds; 
   #  $weektime     += $seconds;
   #  my $charge = $rate_detail->conn_charge; 
-  my $seconds = 0;
+  #my $seconds = 0;
   my $charge = 0;
   my $connection_charged = 0;
 
   my $charge = 0;
   my $connection_charged = 0;
 
@@ -823,11 +881,6 @@ sub rate_prefix {
 
     $seconds_left -= $charge_sec;
 
 
     $seconds_left -= $charge_sec;
 
-    my $included_min = $opt{'region_group_included_min_hashref'} || {};
-
-    $included_min->{$regionnum}{$ratetimenum} = $rate_detail->min_included
-      unless exists $included_min->{$regionnum}{$ratetimenum};
-
     my $granularity = $rate_detail->sec_granularity;
 
     my $minutes;
     my $granularity = $rate_detail->sec_granularity;
 
     my $minutes;
@@ -843,22 +896,53 @@ sub rate_prefix {
       $seconds_left = 0;
     }
 
       $seconds_left = 0;
     }
 
-    $seconds += $charge_sec;
-
-
-    my $region_group = ($part_pkg->option_cacheable('min_included') || 0) > 0;
-
-    ${$opt{region_group_included_min}} -= $minutes 
-        if $region_group && $rate_detail->region_group;
+    #$seconds += $charge_sec;
+
+    if ( $rate_detail->min_included ) {
+      # the old, kind of deprecated way to do this:
+      # 
+      # The rate detail itself has included minutes.  We MUST have a place
+      # to track them.
+      my $included_min = $opt{'detail_included_min_hashref'}
+        or return "unable to rate CDR: rate detail has included minutes, but ".
+                  "no detail_included_min_hashref provided.\n";
+
+      # by default, set the included minutes for this region/time to
+      # what's in the rate_detail
+      $included_min->{$regionnum}{$ratetimenum} = $rate_detail->min_included
+        unless exists $included_min->{$regionnum}{$ratetimenum};
+
+      if ( $included_min->{$regionnum}{$ratetimenum} >= $minutes ) {
+        $charge_sec = 0;
+        $included_min->{$regionnum}{$ratetimenum} -= $minutes;
+      } else {
+        $charge_sec -= ($included_min->{$regionnum}{$ratetimenum} * 60);
+        $included_min->{$regionnum}{$ratetimenum} = 0;
+      }
+    } elsif ( $opt{plan_included_min} && ${ $opt{plan_included_min} } > 0 ) {
+      # The package definition has included minutes, but only for in-group
+      # rate details.  Decrement them if this is an in-group call.
+      if ( $rate_detail->region_group ) {
+        if ( ${ $opt{'plan_included_min'} } >= $minutes ) {
+          $charge_sec = 0;
+          ${ $opt{'plan_included_min'} } -= $minutes;
+        } else {
+          $charge_sec -= (${ $opt{'plan_included_min'} } * 60);
+          ${ $opt{'plan_included_min'} } = 0;
+        }
+      }
+    } else {
+      # the new way!
+      my $applied_min = $cust_pkg->apply_usage(
+        'cdr'         => $self,
+        'rate_detail' => $rate_detail,
+        'minutes'     => $minutes
+      );
+      # for now, usage pools deal only in whole minutes
+      $charge_sec -= $applied_min * 60;
+    }
 
 
-    $included_min->{$regionnum}{$ratetimenum} -= $minutes;
-    if (
-         $included_min->{$regionnum}{$ratetimenum} <= 0
-         && ( ${$opt{region_group_included_min}} <= 0
-              || ! $rate_detail->region_group
-            )
-       )
-    {
+    if ( $charge_sec > 0 ) {
 
       #NOW do connection charges here... right?
       #my $conn_seconds = min($seconds_left, $rate_detail->conn_sec);
 
       #NOW do connection charges here... right?
       #my $conn_seconds = min($seconds_left, $rate_detail->conn_sec);
@@ -871,16 +955,13 @@ sub rate_prefix {
       }
 
                            #should preserve (display?) this
       }
 
                            #should preserve (display?) this
-      my $charge_min = 0 - $included_min->{$regionnum}{$ratetimenum} - ( $conn_seconds / 60 );
-      $included_min->{$regionnum}{$ratetimenum} = 0;
-      $charge += ($rate_detail->min_charge * $charge_min) if $charge_min > 0; #still not rounded
-
-    } elsif ( ${$opt{region_group_included_min}} > 0
-              && $region_group
-              && $rate_detail->region_group 
-           )
-    {
-        $included_min->{$regionnum}{$ratetimenum} = 0 
+      if ( $granularity == 0 ) { # per call rate
+        $charge += $rate_detail->min_charge;
+      } else {
+        my $charge_min = ( $charge_sec - $conn_seconds ) / 60;
+        $charge += ($rate_detail->min_charge * $charge_min) if $charge_min > 0; #still not rounded
+      }
+
     }
 
     # choose next rate_detail
     }
 
     # choose next rate_detail
@@ -897,13 +978,19 @@ sub rate_prefix {
   # this is why we need regionnum/rate_region....
   warn "  (rate region $rate_region)\n" if $DEBUG;
 
   # this is why we need regionnum/rate_region....
   warn "  (rate region $rate_region)\n" if $DEBUG;
 
+  # NOW round it.
+  my $rounding = $part_pkg->option_cacheable('rounding') || 2;
+  my $sprintformat = '%.'. $rounding. 'f';
+  my $roundup = 10**(-3-$rounding);
+  my $price = sprintf($sprintformat, $charge + $roundup);
+
   $self->set_status_and_rated_price(
     'rated',
   $self->set_status_and_rated_price(
     'rated',
-    sprintf('%.2f', $charge + 0.000001), # NOW round it.
+    $price,
     $opt{'svcnum'},
     'rated_pretty_dst'    => $pretty_dst,
     'rated_regionname'    => $rate_region->regionname,
     $opt{'svcnum'},
     'rated_pretty_dst'    => $pretty_dst,
     'rated_regionname'    => $rate_region->regionname,
-    'rated_seconds'       => $seconds,
+    'rated_seconds'       => $rated_seconds, #$seconds,
     'rated_granularity'   => $rate_detail->sec_granularity, #$granularity
     'rated_ratedetailnum' => $rate_detail->ratedetailnum,
     'rated_classnum'      => $rate_detail->classnum, #rated_ratedetailnum?
     'rated_granularity'   => $rate_detail->sec_granularity, #$granularity
     'rated_ratedetailnum' => $rate_detail->ratedetailnum,
     'rated_classnum'      => $rate_detail->classnum, #rated_ratedetailnum?
@@ -920,6 +1007,8 @@ sub rate_upstream_simple {
     sprintf('%.3f', $self->upstream_price),
     $opt{'svcnum'},
     'rated_classnum' => $self->calltypenum,
     sprintf('%.3f', $self->upstream_price),
     $opt{'svcnum'},
     'rated_classnum' => $self->calltypenum,
+    'rated_seconds'  => $self->billsec,
+    # others? upstream_*_regionname => rated_regionname is possible
   );
 }
 
   );
 }
 
@@ -946,12 +1035,12 @@ sub rate_single_price {
 
   my $charge_min = $minutes;
 
 
   my $charge_min = $minutes;
 
-  ${$opt{single_price_included_min}} -= $minutes;
-  if ( ${$opt{single_price_included_min}} > 0 ) {
+  ${$opt{plan_included_min}} -= $minutes;
+  if ( ${$opt{plan_included_min}} > 0 ) {
     $charge_min = 0;
   } else {
     $charge_min = 0;
   } else {
-     $charge_min = 0 - ${$opt{single_price_included_min}};
-     ${$opt{single_price_included_min}} = 0;
+     $charge_min = 0 - ${$opt{plan_included_min}};
+     ${$opt{plan_included_min}} = 0;
   }
 
   my $charge =
   }
 
   my $charge =
@@ -968,6 +1057,31 @@ sub rate_single_price {
 
 }
 
 
 }
 
+=item rate_cost
+
+Rates an already-rated CDR according to the cost fields from the rate plan.
+
+Returns the amount.
+
+=cut
+
+sub rate_cost {
+  my $self = shift;
+
+  return 0 unless $self->rated_ratedetailnum;
+
+  my $rate_detail =
+    qsearchs('rate_detail', { 'ratedetailnum' => $self->rated_ratedetailnum } );
+
+  return $rate_detail->min_cost if $self->rated_granularity == 0;
+
+  my $minutes = $self->rated_seconds / 60;
+  my $charge = $rate_detail->conn_cost + $minutes * $rate_detail->min_cost;
+
+  sprintf('%.2f', $charge + .00001 );
+
+}
+
 =item cdr_termination [ TERMPART ]
 
 =cut
 =item cdr_termination [ TERMPART ]
 
 =cut
@@ -1077,6 +1191,8 @@ sub calltypename {
 
 =cut
 
 
 =cut
 
+# in the future, load this dynamically from detail_format classes
+
 my %export_names = (
   'simple'  => {
     'name'           => 'Simple',
 my %export_names = (
   'simple'  => {
     'name'           => 'Simple',
@@ -1095,6 +1211,10 @@ my %export_names = (
     'name'           => 'Basic',
     'invoice_header' => "Date/Time,Called Number,Min/Sec,Price",
   },
     'name'           => 'Basic',
     'invoice_header' => "Date/Time,Called Number,Min/Sec,Price",
   },
+  'basic_upstream_dst_regionname' => {
+    'name'           => 'Basic with upstream destination name',
+    'invoice_header' => "Date/Time,Called Number,Destination,Min/Sec,Price",
+  },
   'default' => {
     'name'           => 'Default',
     'invoice_header' => 'Date,Time,Number,Destination,Duration,Price',
   'default' => {
     'name'           => 'Default',
     'invoice_header' => 'Date,Time,Number,Destination,Duration,Price',
@@ -1123,6 +1243,10 @@ my %export_names = (
     'name'           => 'Summary, one line per destination prefix',
     'invoice_header' => 'Caller,Rate,Calls,Minutes,Price',
   },
     'name'           => 'Summary, one line per destination prefix',
     'invoice_header' => 'Caller,Rate,Calls,Minutes,Price',
   },
+  'sum_count_class' => {
+    'name'           => 'Summary, one line per usage class',
+    'invoice_header' => 'Caller,Class,Calls,Price',
+  },
 );
 
 my %export_formats = ();
 );
 
 my %export_formats = ();
@@ -1134,7 +1258,7 @@ sub export_formats {
   my $conf = new FS::Conf;
   my $date_format = $conf->config('date_format') || '%m/%d/%Y';
 
   my $conf = new FS::Conf;
   my $date_format = $conf->config('date_format') || '%m/%d/%Y';
 
-  # call duration in the largest units that accurately reflect the  granularity
+  # call duration in the largest units that accurately reflect the granularity
   my $duration_sub = sub {
     my($cdr, %opt) = @_;
     my $sec = $opt{seconds} || $cdr->billsec;
   my $duration_sub = sub {
     my($cdr, %opt) = @_;
     my $sec = $opt{seconds} || $cdr->billsec;
@@ -1168,6 +1292,8 @@ sub export_formats {
     length($price) ? ($opt{money_char} . $price) : '';
   };
 
     length($price) ? ($opt{money_char} . $price) : '';
   };
 
+  my $src_sub = sub { $_[0]->clid || $_[0]->src };
+
   %export_formats = (
     'simple' => [
       sub { time2str($date_format, shift->calldate_unix ) },   #DATE
   %export_formats = (
     'simple' => [
       sub { time2str($date_format, shift->calldate_unix ) },   #DATE
@@ -1182,7 +1308,7 @@ sub export_formats {
       sub { time2str($date_format, shift->calldate_unix ) },   #DATE
       sub { time2str('%r', shift->calldate_unix ) },   #TIME
       #'userfield',                                     #USER
       sub { time2str($date_format, shift->calldate_unix ) },   #DATE
       sub { time2str('%r', shift->calldate_unix ) },   #TIME
       #'userfield',                                     #USER
-      'src',                                           #called from
+      $src_sub,                                           #called from
       'dst',                                           #NUMBER_DIALED
       $duration_sub,                                   #DURATION
       #sub { sprintf('%.3f', shift->upstream_price ) }, #PRICE
       'dst',                                           #NUMBER_DIALED
       $duration_sub,                                   #DURATION
       #sub { sprintf('%.3f', shift->upstream_price ) }, #PRICE
@@ -1191,7 +1317,7 @@ sub export_formats {
     'accountcode_simple' => [
       sub { time2str($date_format, shift->calldate_unix ) },   #DATE
       sub { time2str('%r', shift->calldate_unix ) },   #TIME
     'accountcode_simple' => [
       sub { time2str($date_format, shift->calldate_unix ) },   #DATE
       sub { time2str('%r', shift->calldate_unix ) },   #TIME
-      'src',                                           #called from
+      $src_sub,                                           #called from
       'accountcode',                                   #NUMBER_DIALED
       $duration_sub,                                   #DURATION
       $price_sub,
       'accountcode',                                   #NUMBER_DIALED
       $duration_sub,                                   #DURATION
       $price_sub,
@@ -1199,14 +1325,14 @@ sub export_formats {
     'sum_duration' => [ 
       # for summary formats, the CDR is a fictitious object containing the 
       # total billsec and the phone number of the service
     'sum_duration' => [ 
       # for summary formats, the CDR is a fictitious object containing the 
       # total billsec and the phone number of the service
-      'src',
+      $src_sub,
       sub { my($cdr, %opt) = @_; $opt{ratename} },
       sub { my($cdr, %opt) = @_; $opt{count} },
       sub { my($cdr, %opt) = @_; int($opt{seconds}/60).'m' },
       $price_sub,
     ],
     'sum_count' => [
       sub { my($cdr, %opt) = @_; $opt{ratename} },
       sub { my($cdr, %opt) = @_; $opt{count} },
       sub { my($cdr, %opt) = @_; int($opt{seconds}/60).'m' },
       $price_sub,
     ],
     'sum_count' => [
-      'src',
+      $src_sub,
       sub { my($cdr, %opt) = @_; $opt{ratename} },
       sub { my($cdr, %opt) = @_; $opt{count} },
       $price_sub,
       sub { my($cdr, %opt) = @_; $opt{ratename} },
       sub { my($cdr, %opt) = @_; $opt{count} },
       $price_sub,
@@ -1240,7 +1366,7 @@ sub export_formats {
       $price_sub,
     ],
   );
       $price_sub,
     ],
   );
-  $export_formats{'source_default'} = [ 'src', @{ $export_formats{'default'} }, ];
+  $export_formats{'source_default'} = [ $src_sub, @{ $export_formats{'default'} }, ];
   $export_formats{'accountcode_default'} =
     [ @{ $export_formats{'default'} }[0,1],
       'accountcode',
   $export_formats{'accountcode_default'} =
     [ @{ $export_formats{'default'} }[0,1],
       'accountcode',
@@ -1248,7 +1374,7 @@ sub export_formats {
     ];
   my @default = @{ $export_formats{'default'} };
   $export_formats{'description_default'} = 
     ];
   my @default = @{ $export_formats{'default'} };
   $export_formats{'description_default'} = 
-    [ 'src', @default[0..2], 
+    [ $src_sub, @default[0..2], 
       sub { my($cdr, %opt) = @_; $cdr->description },
       @default[4,5] ];
 
       sub { my($cdr, %opt) = @_; $cdr->description },
       @default[4,5] ];
 
@@ -1286,8 +1412,6 @@ sub downstream_csv {
   #$opt{'money_char'} ||= $conf->config('money_char') || '$';
   $opt{'money_char'} ||= FS::Conf->new->config('money_char') || '$';
 
   #$opt{'money_char'} ||= $conf->config('money_char') || '$';
   $opt{'money_char'} ||= FS::Conf->new->config('money_char') || '$';
 
-  eval "use Text::CSV_XS;";
-  die $@ if $@;
   my $csv = new Text::CSV_XS;
 
   my @columns =
   my $csv = new Text::CSV_XS;
 
   my @columns =
@@ -1403,8 +1527,8 @@ as keys (for use with batch_import) and "pretty" format names as values.
 
 my %cdr_info;
 foreach my $INC ( @INC ) {
 
 my %cdr_info;
 foreach my $INC ( @INC ) {
-  warn "globbing $INC/FS/cdr/*.pm\n" if $DEBUG;
-  foreach my $file ( glob("$INC/FS/cdr/*.pm") ) {
+  warn "globbing $INC/FS/cdr/[a-z]*.pm\n" if $DEBUG;
+  foreach my $file ( glob("$INC/FS/cdr/[a-z]*.pm") ) {
     warn "attempting to load CDR format info from $file\n" if $DEBUG;
     $file =~ /\/(\w+)\.pm$/ or do {
       warn "unrecognized file in $INC/FS/cdr/: $file\n";
     warn "attempting to load CDR format info from $file\n" if $DEBUG;
     $file =~ /\/(\w+)\.pm$/ or do {
       warn "unrecognized file in $INC/FS/cdr/: $file\n";
@@ -1578,9 +1702,20 @@ my %import_options = (
           keys %cdr_info
     },
 
           keys %cdr_info
     },
 
-  'format_row_callbacks' => { map { $_ => $cdr_info{$_}->{'row_callback'}; }
-                                  keys %cdr_info
-                            },
+  'format_asn_formats' =>
+    { map { $_ => $cdr_info{$_}->{'asn_format'}; }
+          keys %cdr_info
+    },
+
+  'format_row_callbacks' =>
+    { map { $_ => $cdr_info{$_}->{'row_callback'}; }
+          keys %cdr_info
+    },
+
+  'format_parser_opts' =>
+    { map { $_ => $cdr_info{$_}->{'parser_opt'}; }
+          keys %cdr_info
+    },
 );
 
 sub _import_options {
 );
 
 sub _import_options {