default to a session cookie instead of setting an explicit timeout, weird timezone...
[freeside.git] / FS / FS / cdr.pm
index 4c02b06..e0c4bd4 100644 (file)
@@ -3,6 +3,9 @@ package FS::cdr;
 use strict;
 use vars qw( @ISA @EXPORT_OK $DEBUG $me
              $conf $cdr_prerate %cdr_prerate_cdrtypenums
 use strict;
 use vars qw( @ISA @EXPORT_OK $DEBUG $me
              $conf $cdr_prerate %cdr_prerate_cdrtypenums
+             $use_lrn $support_key $max_duration
+             $cp_accountcode $cp_accountcode_trim0s $cp_field
+             $tollfree_country
            );
 use Exporter;
 use List::Util qw(first min);
            );
 use Exporter;
 use List::Util qw(first min);
@@ -24,8 +27,14 @@ use FS::rate;
 use FS::rate_prefix;
 use FS::rate_detail;
 
 use FS::rate_prefix;
 use FS::rate_detail;
 
+# LRN lookup
+use LWP::UserAgent;
+use HTTP::Request::Common qw(POST);
+use IO::Socket::SSL;
+use Cpanel::JSON::XS qw(decode_json);
+
 @ISA = qw(FS::Record);
 @ISA = qw(FS::Record);
-@EXPORT_OK = qw( _cdr_date_parser_maker _cdr_min_parser_maker );
+@EXPORT_OK = qw( _cdr_date_parser_maker _cdr_min_parser_maker _cdr_date_parse );
 
 $DEBUG = 0;
 $me = '[FS::cdr]';
 
 $DEBUG = 0;
 $me = '[FS::cdr]';
@@ -39,6 +48,19 @@ FS::UID->install_callback( sub {
   @cdr_prerate_cdrtypenums = $conf->config('cdr-prerate-cdrtypenums')
     if $cdr_prerate;
   %cdr_prerate_cdrtypenums = map { $_=>1 } @cdr_prerate_cdrtypenums;
   @cdr_prerate_cdrtypenums = $conf->config('cdr-prerate-cdrtypenums')
     if $cdr_prerate;
   %cdr_prerate_cdrtypenums = map { $_=>1 } @cdr_prerate_cdrtypenums;
+
+  $support_key = $conf->config('support-key');
+  $use_lrn = $conf->exists('cdr-lrn_lookup');
+
+  $max_duration = $conf->config('cdr-max_duration') || 0;
+
+  $cp_accountcode = $conf->exists('cdr-charged_party-accountcode');
+  $cp_accountcode_trim0s = $conf->exists('cdr-charged_party-accountcode-trim_leading_0s');
+
+  $cp_field = $conf->config('cdr-charged_party-field');
+
+  $tollfree_country = $conf->config('tollfree-country') || '';
+
 });
 
 =head1 NAME
 });
 
 =head1 NAME
@@ -159,7 +181,11 @@ following fields are currently supported:
 
 =item freesiderewritestatus - NULL, done, skipped
 
 
 =item freesiderewritestatus - NULL, done, skipped
 
-=item cdrbatch
+=item cdrbatchnum
+
+=item detailnum - Link to invoice detail (L<FS::cust_bill_pkg_detail>)
+
+=item sipcallid - SIP Call-ID
 
 =back
 
 
 =back
 
@@ -213,7 +239,10 @@ sub table_info {
         'upstream_price'        => 'Upstream price',
         #'upstream_rateplanid'   => '',
         #'ratedetailnum'         => '',
         'upstream_price'        => 'Upstream price',
         #'upstream_rateplanid'   => '',
         #'ratedetailnum'         => '',
+        'src_lrn'               => 'Source LRN',
+        'dst_lrn'               => 'Dest. LRN',
         'rated_price'           => 'Rated price',
         'rated_price'           => 'Rated price',
+        'rated_cost'            => 'Rated cost',
         #'distance'              => '',
         #'islocal'               => '',
         #'calltypenum'           => '',
         #'distance'              => '',
         #'islocal'               => '',
         #'calltypenum'           => '',
@@ -224,8 +253,8 @@ sub table_info {
         'svcnum'                => 'Freeside service',
         'freesidestatus'        => 'Freeside status',
         'freesiderewritestatus' => 'Freeside rewrite status',
         'svcnum'                => 'Freeside service',
         'freesidestatus'        => 'Freeside status',
         'freesiderewritestatus' => 'Freeside rewrite status',
-        'cdrbatch'              => 'Legacy batch',
         'cdrbatchnum'           => 'Batch',
         'cdrbatchnum'           => 'Batch',
+        'detailnum'             => 'Freeside invoice detail line',
     },
 
   };
     },
 
   };
@@ -337,8 +366,12 @@ sub check {
 
   #check the foreign keys even?
   #do we want to outright *reject* the CDR?
 
   #check the foreign keys even?
   #do we want to outright *reject* the CDR?
-  my $error =
-       $self->ut_numbern('acctid');
+  my $error = $self->ut_numbern('acctid');
+  return $error if $error;
+
+  if ( $self->freesidestatus ne 'done' ) {
+    $self->set('detailnum', ''); # can't have this on an unbilled call
+  }
 
   #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
   #
@@ -351,8 +384,6 @@ 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;
 }
 
   $self->SUPER::check;
 }
 
@@ -368,10 +399,9 @@ 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';
-  my $country = $conf->config('tollfree-country') || '';
-  if ( $country eq 'AU' ) { 
+  if ( $tollfree_country eq 'AU' ) { 
     ( $self->$field() =~ /^(\+?61)?(1800|1300)/ ) ? 1 : 0;
     ( $self->$field() =~ /^(\+?61)?(1800|1300)/ ) ? 1 : 0;
-  } elsif ( $country eq 'NZ' ) { 
+  } elsif ( $tollfree_country eq 'NZ' ) { 
     ( $self->$field() =~ /^(\+?64)?(800|508)/ ) ? 1 : 0;
   } else { #NANPA (US/Canaada)
     ( $self->$field() =~ /^(\+?1)?8(8|([02-7])\3)/ ) ? 1 : 0;
     ( $self->$field() =~ /^(\+?64)?(800|508)/ ) ? 1 : 0;
   } else { #NANPA (US/Canaada)
     ( $self->$field() =~ /^(\+?1)?8(8|([02-7])\3)/ ) ? 1 : 0;
@@ -397,17 +427,16 @@ sub set_charged_party {
 
   unless ( $self->charged_party ) {
 
 
   unless ( $self->charged_party ) {
 
-    if ( $conf->exists('cdr-charged_party-accountcode') && $self->accountcode ){
+    if ( $cp_accountcode && $self->accountcode ) {
 
       my $charged_party = $self->accountcode;
       $charged_party =~ s/^0+//
 
       my $charged_party = $self->accountcode;
       $charged_party =~ s/^0+//
-        if $conf->exists('cdr-charged_party-accountcode-trim_leading_0s');
+        if $cp_accountcode_trim0s;
       $self->charged_party( $charged_party );
 
       $self->charged_party( $charged_party );
 
-    } elsif ( $conf->exists('cdr-charged_party-field') ) {
+    } elsif ( $cp_field ) {
 
 
-      my $field = $conf->config('cdr-charged_party-field');
-      $self->charged_party( $self->$field() );
+      $self->charged_party( $self->$cp_field() );
 
     } else {
 
 
     } else {
 
@@ -463,7 +492,9 @@ Sets the status and rated price.
 
 Available options are: inbound, rated_pretty_dst, rated_regionname,
 rated_seconds, rated_minutes, rated_granularity, rated_ratedetailnum,
 
 Available options are: inbound, rated_pretty_dst, rated_regionname,
 rated_seconds, rated_minutes, rated_granularity, rated_ratedetailnum,
-rated_classnum, rated_ratename.
+rated_classnum, rated_ratename.  If rated_ratedetailnum is provided,
+will also set a recalculated L</rate_cost> in the rated_cost field 
+after the other fields are set (does not work with inbound.)
 
 If there is an error, returns the error, otherwise returns false.
 
 
 If there is an error, returns the error, otherwise returns false.
 
@@ -487,20 +518,24 @@ sub set_status_and_rated_price {
         rated_price => $rated_price,
         status      => $status,
     });
         rated_price => $rated_price,
         status      => $status,
     });
-    $term->rated_seconds($opt{rated_seconds}) if exists($opt{rated_seconds});
-    $term->rated_minutes($opt{rated_minutes}) if exists($opt{rated_minutes});
+    foreach (qw(rated_seconds rated_minutes rated_granularity)) {
+      $term->set($_, $opt{$_}) if exists($opt{$_});
+    }
     $term->svcnum($svcnum) if $svcnum;
     return $term->insert;
 
   } else {
 
     $self->freesidestatus($status);
     $term->svcnum($svcnum) if $svcnum;
     return $term->insert;
 
   } else {
 
     $self->freesidestatus($status);
+    $self->freesidestatustext($opt{'statustext'}) if exists($opt{'statustext'});
     $self->rated_price($rated_price);
     $self->$_($opt{$_})
       foreach grep exists($opt{$_}), map "rated_$_",
         qw( pretty_dst regionname seconds minutes granularity
             ratedetailnum classnum ratename );
     $self->svcnum($svcnum) if $svcnum;
     $self->rated_price($rated_price);
     $self->$_($opt{$_})
       foreach grep exists($opt{$_}), map "rated_$_",
         qw( pretty_dst regionname seconds minutes granularity
             ratedetailnum classnum ratename );
     $self->svcnum($svcnum) if $svcnum;
+    $self->rated_cost($self->rate_cost) if $opt{'rated_ratedetailnum'};
+
     return $self->replace();
 
   }
     return $self->replace();
 
   }
@@ -536,6 +571,9 @@ sub parse_number {
 
   my $field = $options{column} || 'dst';
   my $intl = $options{international_prefix} || '011';
 
   my $field = $options{column} || 'dst';
   my $intl = $options{international_prefix} || '011';
+  # Still, don't break anyone's CDR rating if they have an empty string in
+  # there. Require an explicit statement that there's no prefix.
+  $intl = '' if lc($intl) eq 'none';
   my $countrycode = '';
   my $number = $self->$field();
 
   my $countrycode = '';
   my $number = $self->$field();
 
@@ -622,6 +660,10 @@ sub rate_prefix {
   my $part_pkg = $opt{'part_pkg'} or return "No part_pkg specified";
   my $cust_pkg = $opt{'cust_pkg'};
 
   my $part_pkg = $opt{'part_pkg'} or return "No part_pkg specified";
   my $cust_pkg = $opt{'cust_pkg'};
 
+  ###
+  # (Directory assistance) rewriting
+  ###
+
   my $da_rewrote = 0;
   # this will result in those CDRs being marked as done... is that 
   # what we want?
   my $da_rewrote = 0;
   # this will result in those CDRs being marked as done... is that 
   # what we want?
@@ -637,6 +679,10 @@ sub rate_prefix {
     $da_rewrote = 1;
   }
 
     $da_rewrote = 1;
   }
 
+  ###
+  # Checks to see if the CDR is chargeable
+  ###
+
   my $reason = $part_pkg->check_chargable( $self,
                                            'da_rewrote'   => $da_rewrote,
                                          );
   my $reason = $part_pkg->check_chargable( $self,
                                            'da_rewrote'   => $da_rewrote,
                                          );
@@ -645,6 +691,7 @@ sub rate_prefix {
     return $self->set_status_and_rated_price( 'skipped',
                                               0,
                                               $opt{'svcnum'},
     return $self->set_status_and_rated_price( 'skipped',
                                               0,
                                               $opt{'svcnum'},
+                                              'statustext' => $reason,
                                             );
   }
 
                                             );
   }
 
@@ -673,8 +720,16 @@ sub rate_prefix {
     }
   }
 
     }
   }
 
-    
-
+  my $rated_seconds = $part_pkg->option_cacheable('use_duration')
+                        ? $self->duration
+                        : $self->billsec;
+  if ( $max_duration > 0 && $rated_seconds > $max_duration ) {
+    return $self->set_status_and_rated_price(
+      'failed',
+      '',
+      $opt{'svcnum'},
+    );
+  }
 
   ###
   # look up rate details based on called station id
 
   ###
   # look up rate details based on called station id
@@ -709,13 +764,32 @@ sub rate_prefix {
     domestic_prefix => $part_pkg->option_cacheable('domestic_prefix'),
   );
 
     domestic_prefix => $part_pkg->option_cacheable('domestic_prefix'),
   );
 
+  my $ratename = '';
+  my $intrastate_ratenum = $part_pkg->option_cacheable('intrastate_ratenum');
+
+  if ( $use_lrn and $countrycode eq '1' ) {
+
+    # then ask about the number
+    foreach my $field ('src', 'dst') {
+
+      $self->get_lrn($field);
+      if ( $field eq $column ) {
+        # then we are rating on this number
+        $number = $self->get($field.'_lrn');
+        $number =~ s/^1//;
+        # is this ever meaningful? can the LRN be outside NANP space?
+      }
+
+    } # foreach $field
+
+  }
+
   warn "rating call $to_or_from +$countrycode $number\n" if $DEBUG;
   my $pretty_dst = "+$countrycode $number";
   #asterisks here causes inserting the detail to barf, so:
   $pretty_dst =~ s/\*//g;
 
   warn "rating call $to_or_from +$countrycode $number\n" if $DEBUG;
   my $pretty_dst = "+$countrycode $number";
   #asterisks here causes inserting the detail to barf, so:
   $pretty_dst =~ s/\*//g;
 
-  my $ratename = '';
-  my $intrastate_ratenum = $part_pkg->option_cacheable('intrastate_ratenum');
+  # should check $countrycode eq '1' here?
   if ( $intrastate_ratenum && !$self->is_tollfree ) {
     $ratename = 'Interstate'; #until proven otherwise
     # this is relatively easy only because:
   if ( $intrastate_ratenum && !$self->is_tollfree ) {
     $ratename = 'Interstate'; #until proven otherwise
     # this is relatively easy only because:
@@ -724,8 +798,10 @@ sub rate_prefix {
     # -disregard private or unknown numbers
     # -there is exactly one record in rate_prefix for a given NPANXX
     # -default to interstate if we can't find one or both of the prefixes
     # -disregard private or unknown numbers
     # -there is exactly one record in rate_prefix for a given NPANXX
     # -default to interstate if we can't find one or both of the prefixes
+    my $dst_col = $use_lrn ? 'dst_lrn' : 'dst';
+    my $src_col = $use_lrn ? 'src_lrn' : 'src';
     my (undef, $dstprefix) = $self->parse_number(
     my (undef, $dstprefix) = $self->parse_number(
-      column => 'dst',
+      column => $dst_col,
       international_prefix => $part_pkg->option_cacheable('international_prefix'),
       domestic_prefix => $part_pkg->option_cacheable('domestic_prefix'),
     );
       international_prefix => $part_pkg->option_cacheable('international_prefix'),
       domestic_prefix => $part_pkg->option_cacheable('domestic_prefix'),
     );
@@ -734,7 +810,7 @@ sub rate_prefix {
                                                 'npa' => $1, 
                                          }) || '';
     my (undef, $srcprefix) = $self->parse_number(
                                                 'npa' => $1, 
                                          }) || '';
     my (undef, $srcprefix) = $self->parse_number(
-      column => 'src',
+      column => $src_col,
       international_prefix => $part_pkg->option_cacheable('international_prefix'),
       domestic_prefix => $part_pkg->option_cacheable('domestic_prefix'),
     );
       international_prefix => $part_pkg->option_cacheable('international_prefix'),
       domestic_prefix => $part_pkg->option_cacheable('domestic_prefix'),
     );
@@ -827,9 +903,6 @@ 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 $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
   my $seconds_left = $rated_seconds;
 
   #no, do this later so it respects (group) included minutes
@@ -914,8 +987,10 @@ sub rate_prefix {
 
       # by default, set the included minutes for this region/time to
       # what's in the rate_detail
 
       # 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 (!exists( $included_min->{$regionnum}{$ratetimenum} )) {
+        $included_min->{$regionnum}{$ratetimenum} =
+          ($rate_detail->min_included * $cust_pkg->quantity || 1);
+      }
 
       if ( $included_min->{$regionnum}{$ratetimenum} >= $minutes ) {
         $charge_sec = 0;
 
       if ( $included_min->{$regionnum}{$ratetimenum} >= $minutes ) {
         $charge_sec = 0;
@@ -1249,6 +1324,10 @@ my %export_names = (
     'name'           => 'Number of calls, one line per service',
     'invoice_header' => 'Caller,Rate,Messages,Price',
   },
     'name'           => 'Number of calls, one line per service',
     'invoice_header' => 'Caller,Rate,Messages,Price',
   },
+  'sum_duration' => {
+    'name'           => 'Summary, one line per service',
+    'invoice_header' => 'Caller,Rate,Calls,Minutes,Price',
+  },
   'sum_duration_prefix' => {
     'name'           => 'Summary, one line per destination prefix',
     'invoice_header' => 'Caller,Rate,Calls,Minutes,Price',
   'sum_duration_prefix' => {
     'name'           => 'Summary, one line per destination prefix',
     'invoice_header' => 'Caller,Rate,Calls,Minutes,Price',
@@ -1257,6 +1336,10 @@ my %export_names = (
     'name'           => 'Summary, one line per usage class',
     'invoice_header' => 'Caller,Class,Calls,Price',
   },
     'name'           => 'Summary, one line per usage class',
     'invoice_header' => 'Caller,Class,Calls,Price',
   },
+  'sum_duration_accountcode' => {
+    'name'           => 'Summary, one line per accountcode',
+    'invoice_header' => 'Caller,Rate,Calls,Minutes,Price',
+  },
 );
 
 my %export_formats = ();
 );
 
 my %export_formats = ();
@@ -1440,6 +1523,44 @@ sub downstream_csv {
 
 }
 
 
 }
 
+sub get_lrn {
+  my $self = shift;
+  my $field = shift;
+
+  my $ua = LWP::UserAgent->new(
+             'ssl_opts' => {
+               verify_hostname => 0,
+               SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE,
+             },
+           );
+
+  my $url = 'https://ws.freeside.biz/get_lrn';
+
+  my %content = ( 'support-key' => $support_key,
+                  'tn' => $self->get($field),
+                );
+  my $response = $ua->request( POST $url, \%content );
+
+  die "LRN service error: ". $response->message. "\n"
+    unless $response->is_success;
+
+  local $@;
+  my $data = eval { decode_json($response->content) };
+  die "LRN service JSON error : $@\n" if $@;
+
+  if ($data->{error}) {
+    die "acctid ".$self->acctid." $field LRN lookup failed:\n$data->{error}";
+    # for testing; later we should respect ignore_unrateable
+  } elsif ($data->{lrn}) {
+    # normal case
+    $self->set($field.'_lrn', $data->{lrn});
+  } else {
+    die "acctid ".$self->acctid." $field LRN lookup returned no number.\n";
+  }
+
+  return $data; # in case it's interesting somehow
+}
 =back
 
 =head1 CLASS METHODS
 =back
 
 =head1 CLASS METHODS
@@ -1458,7 +1579,7 @@ as keys (for use with part_pkg::voip_cdr) and "pretty" format names as values.
 sub invoice_formats {
   map { ($_ => $export_names{$_}->{'name'}) }
     grep { $export_names{$_}->{'invoice_header'} }
 sub invoice_formats {
   map { ($_ => $export_names{$_}->{'name'}) }
     grep { $export_names{$_}->{'invoice_header'} }
-    keys %export_names;
+    sort keys %export_names;
 }
 
 =item invoice_header FORMAT
 }
 
 =item invoice_header FORMAT
@@ -1566,7 +1687,12 @@ foreach my $INC ( @INC ) {
 
 tie my %import_formats, 'Tie::IxHash',
   map  { $_ => $cdr_info{$_}->{'name'} }
 
 tie my %import_formats, 'Tie::IxHash',
   map  { $_ => $cdr_info{$_}->{'name'} }
-  sort { $cdr_info{$a}->{'weight'} <=> $cdr_info{$b}->{'weight'} }
+  
+  #this is not doing anything useful anymore
+  #sort { $cdr_info{$a}->{'weight'} <=> $cdr_info{$b}->{'weight'} }
+  #so just sort alpha
+  sort { lc($cdr_info{$a}->{'name'}) cmp lc($cdr_info{$b}->{'name'}) }
+
   grep { exists($cdr_info{$_}->{'import_fields'}) }
   keys %cdr_info;
 
   grep { exists($cdr_info{$_}->{'import_fields'}) }
   keys %cdr_info;
 
@@ -1627,7 +1753,7 @@ sub _cdr_date_parse {
     # optionally without seconds
     ($mon, $day, $year, $hour, $min, $sec) = ( $1, $2, $3, $4, $5, $6 );
     $sec = 0 if !defined($sec);
     # optionally without seconds
     ($mon, $day, $year, $hour, $min, $sec) = ( $1, $2, $3, $4, $5, $6 );
     $sec = 0 if !defined($sec);
-   } elsif ( $date  =~ /^\s*(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})(\.\d+$)/ ) {
+   } elsif ( $date  =~ /^\s*(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})(\.\d+)$/ ) {
     # broadsoft: 20081223201938.314
     ($year, $mon, $day, $hour, $min, $sec) = ( $1, $2, $3, $4, $5, $6 );
   } elsif ( $date  =~ /^\s*(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})\d+(\D|$)/ ) {
     # broadsoft: 20081223201938.314
     ($year, $mon, $day, $hour, $min, $sec) = ( $1, $2, $3, $4, $5, $6 );
   } elsif ( $date  =~ /^\s*(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})\d+(\D|$)/ ) {
@@ -1637,9 +1763,17 @@ sub _cdr_date_parse {
     # WIP: 20100329121420
     ($year, $mon, $day, $hour, $min, $sec) = ( $1, $2, $3, $4, $5, $6 );
   } elsif ( $date =~ /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})Z$/) {
     # WIP: 20100329121420
     ($year, $mon, $day, $hour, $min, $sec) = ( $1, $2, $3, $4, $5, $6 );
   } elsif ( $date =~ /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})Z$/) {
-    # Telos
+    # Telos 2014-10-10T05:30:33Z
     ($year, $mon, $day, $hour, $min, $sec) = ( $1, $2, $3, $4, $5, $6 );
     $options{gmt} = 1;
     ($year, $mon, $day, $hour, $min, $sec) = ( $1, $2, $3, $4, $5, $6 );
     $options{gmt} = 1;
+  } elsif ( $date =~ /^(\d+):(\d+):(\d+)\.\d+ \w+ (\w+) (\d+) (\d+)$/ ) {
+    ($hour, $min, $sec, $mon, $day, $year) = ( $1, $2, $3, $4, $5, $6 );
+    $mon = { # Acme Packet: 15:54:56.868 PST DEC 18 2017
+      # My best guess of month abbv they may use
+      JAN => '01', FEB => '02', MAR => '03', APR => '04',
+      MAY => '05', JUN => '06', JUL => '07', AUG => '08',
+      SEP => '09', OCT => '10', NOV => '11', DEC => '12'
+    }->{$mon};
   } else {
      die "unparsable date: $date"; #maybe we shouldn't die...
   }
   } else {
      die "unparsable date: $date"; #maybe we shouldn't die...
   }
@@ -1738,10 +1872,11 @@ sub batch_import {
   my $iopt = _import_options;
   $opt->{$_} = $iopt->{$_} foreach keys %$iopt;
 
   my $iopt = _import_options;
   $opt->{$_} = $iopt->{$_} foreach keys %$iopt;
 
-  if ( defined $opt->{'cdrtypenum'} ) {
-        $opt->{'preinsert_callback'} = sub {
-                my($record,$param) = (shift,shift);
-                $record->cdrtypenum($opt->{'cdrtypenum'});
+  if ( grep defined $opt->{$_}, qw(cdrtypenum carrierid) ) {
+        $opt->{preinsert_callback} = sub {
+                my($record, $param) = @_;
+                $record->$_($opt->{$_})
+                  foreach grep defined $opt->{$_}, qw(cdrtypenum carrierid);
                 '';
         };
   }
                 '';
         };
   }
@@ -1767,41 +1902,6 @@ sub process_batch_import {
 #    @columns = map { s/^ +//; $_; } @columns;
 #  }
 
 #    @columns = map { s/^ +//; $_; } @columns;
 #  }
 
-# _ upgrade_data
-#
-# Used by FS::Upgrade to migrate to a new database.
-
-sub _upgrade_data {
-  my ($class, %opts) = @_;
-
-  warn "$me upgrading $class\n" if $DEBUG;
-
-  my $sth = dbh->prepare(
-    'SELECT DISTINCT(cdrbatch) FROM cdr WHERE cdrbatch IS NOT NULL'
-  ) or die dbh->errstr;
-
-  $sth->execute or die $sth->errstr;
-
-  my %cdrbatchnum = ();
-  while (my $row = $sth->fetchrow_arrayref) {
-
-    my $cdr_batch = qsearchs( 'cdr_batch', { 'cdrbatch' => $row->[0] } );
-    unless ( $cdr_batch ) {
-      $cdr_batch = new FS::cdr_batch { 'cdrbatch' => $row->[0] };
-      my $error = $cdr_batch->insert;
-      die $error if $error;
-    }
-
-    $cdrbatchnum{$row->[0]} = $cdr_batch->cdrbatchnum;
-  }
-
-  $sth = dbh->prepare('UPDATE cdr SET cdrbatch = NULL, cdrbatchnum = ? WHERE cdrbatch IS NOT NULL AND cdrbatch = ?') or die dbh->errstr;
-
-  foreach my $cdrbatch (keys %cdrbatchnum) {
-    $sth->execute($cdrbatchnum{$cdrbatch}, $cdrbatch) or die $sth->errstr;
-  }
-
-}
 
 =item ip_addr_sql FIELD RANGE
 
 
 =item ip_addr_sql FIELD RANGE
 
@@ -1839,4 +1939,3 @@ L<FS::Record>, schema.html from the base documentation.
 =cut
 
 1;
 =cut
 
 1;
-