eWay self-signup fixes
[freeside.git] / FS / FS / cdr.pm
index efccd4b..f7402ee 100644 (file)
@@ -1,7 +1,7 @@
 package FS::cdr;
 
 use strict;
 package FS::cdr;
 
 use strict;
-use vars qw( @ISA @EXPORT_OK $DEBUG );
+use vars qw( @ISA @EXPORT_OK $DEBUG $me );
 use Exporter;
 use Tie::IxHash;
 use Date::Parse;
 use Exporter;
 use Tie::IxHash;
 use Date::Parse;
@@ -13,12 +13,14 @@ use FS::Record qw( qsearch qsearchs );
 use FS::cdr_type;
 use FS::cdr_calltype;
 use FS::cdr_carrier;
 use FS::cdr_type;
 use FS::cdr_calltype;
 use FS::cdr_carrier;
-use FS::cdr_upstream_rate;
+use FS::cdr_batch;
+use FS::cdr_termination;
 
 @ISA = qw(FS::Record);
 @EXPORT_OK = qw( _cdr_date_parser_maker _cdr_min_parser_maker );
 
 $DEBUG = 0;
 
 @ISA = qw(FS::Record);
 @EXPORT_OK = qw( _cdr_date_parser_maker _cdr_min_parser_maker );
 
 $DEBUG = 0;
+$me = '[FS::cdr]';
 
 =head1 NAME
 
 
 =head1 NAME
 
@@ -194,7 +196,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'              => 'Batch',
+        'cdrbatch'              => 'Legacy batch',
+        'cdrbatchnum'           => 'Batch',
     },
 
   };
     },
 
   };
@@ -282,6 +285,10 @@ sub check {
 #  ;
 #  return $error if $error;
 
 #  ;
 #  return $error if $error;
 
+  for my $f ( grep { $self->$_ =~ /\D/ } qw(startdate answerdate enddate)){
+    $self->$f( str2time($self->$f) );
+  }
+
   $self->calldate( $self->startdate_sql )
     if !$self->calldate && $self->startdate;
 
   $self->calldate( $self->startdate_sql )
     if !$self->calldate && $self->startdate;
 
@@ -317,15 +324,19 @@ sub check {
   $self->SUPER::check;
 }
 
   $self->SUPER::check;
 }
 
-=item is_tollfree
+=item is_tollfree [ COLUMN ]
 
 
-  Returns true when the cdr represents a toll free number and false otherwise.
+Returns true when the cdr represents a toll free number and false otherwise.
+
+By default, inspects the dst field, but an optional column name can be passed
+to inspect other field.
 
 =cut
 
 sub is_tollfree {
   my $self = shift;
 
 =cut
 
 sub is_tollfree {
   my $self = shift;
-  ( $self->dst =~ /^(\+?1)?8(8|([02-7])\3)/ ) ? 1 : 0;
+  my $field = scalar(@_) ? shift : 'dst';
+  ( $self->$field() =~ /^(\+?1)?8(8|([02-7])\3)/ ) ? 1 : 0;
 }
 
 =item set_charged_party
 }
 
 =item set_charged_party
@@ -354,6 +365,11 @@ sub set_charged_party {
         if $conf->exists('cdr-charged_party-accountcode-trim_leading_0s');
       $self->charged_party( $charged_party );
 
         if $conf->exists('cdr-charged_party-accountcode-trim_leading_0s');
       $self->charged_party( $charged_party );
 
+    } elsif ( $conf->exists('cdr-charged_party-field') ) {
+
+      my $field = $conf->config('cdr-charged_party-field');
+      $self->charged_party( $self->$field() );
+
     } else {
 
       if ( $self->is_tollfree ) {
     } else {
 
       if ( $self->is_tollfree ) {
@@ -376,7 +392,7 @@ sub set_charged_party {
 
 }
 
 
 }
 
-=item set_status_and_rated_price STATUS [ RATED_PRICE ]
+=item set_status_and_rated_price STATUS [ RATED_PRICE [ SVCNUM ] ]
 
 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.
@@ -384,10 +400,33 @@ error, otherwise returns false.
 =cut
 
 sub set_status_and_rated_price {
 =cut
 
 sub set_status_and_rated_price {
-  my($self, $status, $rated_price) = @_;
-  $self->freesidestatus($status);
-  $self->rated_price($rated_price);
-  $self->replace();
+  my($self, $status, $rated_price, $svcnum, %opt) = @_;
+  if($opt{'inbound'}) {
+    my $term = qsearchs('cdr_termination', {
+        acctid   => $self->acctid, 
+        termpart => 1 # inbound
+    });
+    my $error;
+    if($term) {
+      warn "replacing existing cdr status (".$self->acctid.")\n" if $term;
+      $error = $term->delete;
+      return $error if $error;
+    }
+    $term = FS::cdr_termination->new({
+        acctid      => $self->acctid,
+        termpart    => 1,
+        rated_price => $rated_price,
+        status      => $status,
+        svcnum      => $svcnum,
+    });
+    return $term->insert;
+  }
+  else {
+    $self->freesidestatus($status);
+    $self->rated_price($rated_price);
+    $self->svcnum($svcnum) if $svcnum;
+    return $self->replace();
+  }
 }
 
 =item calldate_unix 
 }
 
 =item calldate_unix 
@@ -472,51 +511,11 @@ sub calltypename {
   $cdr_calltype ? $cdr_calltype->calltypename : '';
 }
 
   $cdr_calltype ? $cdr_calltype->calltypename : '';
 }
 
-=item cdr_upstream_rate
-
-Returns the upstream rate mapping (see L<FS::cdr_upstream_rate>), or the empty
-string if no FS::cdr_upstream_rate object is associated with this CDR.
-
-=cut
-
-sub cdr_upstream_rate {
-  my $self = shift;
-  return '' unless $self->upstream_rateid;
-  qsearchs('cdr_upstream_rate', { 'upstream_rateid' => $self->upstream_rateid })
-    or '';
-}
-
-=item _convergent_format COLUMN [ COUNTRYCODE ]
-
-Returns the number in COLUMN formatted as follows:
-
-If the country code does not match COUNTRYCODE (default "61"), it is returned
-unchanged.
-
-If the country code does match COUNTRYCODE (default "61"), it is removed.  In
-addiiton, "0" is prepended unless the number starts with 13, 18 or 19. (???)
-
-=cut
-
-sub _convergent_format {
-  my( $self, $field ) = ( shift, shift );
-  my $countrycode = scalar(@_) ? shift : '61'; #+61 = australia
-  #my $number = $self->$field();
-  my $number = $self->get($field);
-  #if ( $number =~ s/^(\+|011)$countrycode// ) {
-  if ( $number =~ s/^\+$countrycode// ) {
-    $number = "0$number"
-      unless $number =~ /^1[389]/; #???
-  }
-  $number;
-}
-
 =item downstream_csv [ OPTION => VALUE, ... ]
 
 =cut
 
 my %export_names = (
 =item downstream_csv [ OPTION => VALUE, ... ]
 
 =cut
 
 my %export_names = (
-  'convergent'      => {},
   'simple'  => {
     'name'           => 'Simple',
     'invoice_header' => "Date,Time,Name,Destination,Duration,Price",
   'simple'  => {
     'name'           => 'Simple',
     'invoice_header' => "Date,Time,Name,Destination,Duration,Price",
@@ -540,86 +539,92 @@ my %export_names = (
   },
 );
 
   },
 );
 
-my $duration_sub = sub {
-  my($cdr, %opt) = @_;
-  if ( $opt{minutes} ) {
-    $opt{minutes}. ( $opt{granularity} ? 'm' : ' call' );
-  } else {
-    sprintf('%.2fm', $cdr->billsec / 60 );
-  }
-};
-
-my %export_formats = (
-  'convergent' => [
-    'carriername', #CARRIER
-    sub { shift->_convergent_format('src') }, #SERVICE_NUMBER
-    sub { shift->_convergent_format('charged_party') }, #CHARGED_NUMBER
-    sub { time2str('%Y-%m-%d', shift->calldate_unix ) }, #DATE
-    sub { time2str('%T',       shift->calldate_unix ) }, #TIME
-    'billsec', #'duration', #DURATION
-    sub { shift->_convergent_format('dst') }, #NUMBER_DIALED
-    '', #XXX add (from prefixes in most recent email) #FROM_DESC
-    '', #XXX add (from prefixes in most recent email) #TO_DESC
-    'calltypename', #CLASS_CODE
-    'rated_price', #PRICE
-    sub { shift->rated_price ? 'Y' : 'N' }, #RATED
-    '', #OTHER_INFO
-  ],
-  'simple' => [
-    sub { time2str('%D', shift->calldate_unix ) },   #DATE
-    sub { time2str('%r', shift->calldate_unix ) },   #TIME
-    'userfield',                                     #USER
-    'dst',                                           #NUMBER_DIALED
-    $duration_sub,                                   #DURATION
-    #sub { sprintf('%.3f', shift->upstream_price ) }, #PRICE
-    sub { my($cdr, %opt) = @_; $opt{money_char}. $opt{charge}; }, #PRICE
-  ],
-  'simple2' => [
-    sub { time2str('%D', shift->calldate_unix ) },   #DATE
-    sub { time2str('%r', shift->calldate_unix ) },   #TIME
-    #'userfield',                                     #USER
-    'src',                                           #called from
-    'dst',                                           #NUMBER_DIALED
-    $duration_sub,                                   #DURATION
-    #sub { sprintf('%.3f', shift->upstream_price ) }, #PRICE
-    sub { my($cdr, %opt) = @_; $opt{money_char}. $opt{charge}; }, #PRICE
-  ],
-  'default' => [
-
-    #DATE
-    sub { time2str('%D', shift->calldate_unix ) },
-          # #time2str("%Y %b %d - %r", $cdr->calldate_unix ),
-
-    #TIME
-    sub { time2str('%r', shift->calldate_unix ) },
-          # time2str("%c", $cdr->calldate_unix),  #XXX this should probably be a config option dropdown so they can select US vs- rest of world dates or whatnot
-
-    #DEST ("Number")
-    sub { my($cdr, %opt) = @_; $opt{pretty_dst} || $cdr->dst; },
-
-    #REGIONNAME ("Destination")
-    sub { my($cdr, %opt) = @_; $opt{dst_regionname}; },
-
-    #DURATION
-    $duration_sub,
-
-    #PRICE
-    sub { my($cdr, %opt) = @_; $opt{money_char}. $opt{charge}; },
-
-  ],
-);
-$export_formats{'source_default'} = [ 'src', @{ $export_formats{'default'} }, ];
-$export_formats{'accountcode_default'} =
-  [ @{ $export_formats{'default'} }[0,1],
-    'accountcode',
-    @{ $export_formats{'default'} }[2..5],
-  ];
+my %export_formats = ();
+sub export_formats {
+  #my $self = shift;
+
+  return %export_formats if keys %export_formats;
+
+  my $conf = new FS::Conf;
+  my $date_format = $conf->config('date_format') || '%m/%d/%Y';
+
+  # This is now smarter, and shows the call duration in the 
+  # largest units that accurately reflect the granularity.
+  my $duration_sub = sub {
+    my($cdr, %opt) = @_;
+    my $sec = $opt{seconds} || $cdr->billsec;
+    if ( length($opt{granularity}) && 
+         $opt{granularity} == 0 ) { #per call
+      return '1 call';
+    }
+    elsif ( $opt{granularity} == 60 ) {#full minutes
+      return sprintf("%.0fm",$sec/60);
+    }
+    else { #anything else
+      return sprintf("%dm %ds", $sec/60, $sec%60);
+    }
+  };
+
+  %export_formats = (
+    'simple' => [
+      sub { time2str($date_format, shift->calldate_unix ) },   #DATE
+      sub { time2str('%r', shift->calldate_unix ) },   #TIME
+      'userfield',                                     #USER
+      'dst',                                           #NUMBER_DIALED
+      $duration_sub,                                   #DURATION
+      #sub { sprintf('%.3f', shift->upstream_price ) }, #PRICE
+      sub { my($cdr, %opt) = @_; $opt{money_char}. $opt{charge}; }, #PRICE
+    ],
+    'simple2' => [
+      sub { time2str($date_format, shift->calldate_unix ) },   #DATE
+      sub { time2str('%r', shift->calldate_unix ) },   #TIME
+      #'userfield',                                     #USER
+      'src',                                           #called from
+      'dst',                                           #NUMBER_DIALED
+      $duration_sub,                                   #DURATION
+      #sub { sprintf('%.3f', shift->upstream_price ) }, #PRICE
+      sub { my($cdr, %opt) = @_; $opt{money_char}. $opt{charge}; }, #PRICE
+    ],
+    'default' => [
+
+      #DATE
+      sub { time2str($date_format, shift->calldate_unix ) },
+            # #time2str("%Y %b %d - %r", $cdr->calldate_unix ),
+
+      #TIME
+      sub { time2str('%r', shift->calldate_unix ) },
+            # time2str("%c", $cdr->calldate_unix),  #XXX this should probably be a config option dropdown so they can select US vs- rest of world dates or whatnot
+
+      #DEST ("Number")
+      sub { my($cdr, %opt) = @_; $opt{pretty_dst} || $cdr->dst; },
+
+      #REGIONNAME ("Destination")
+      sub { my($cdr, %opt) = @_; $opt{dst_regionname}; },
+
+      #DURATION
+      $duration_sub,
+
+      #PRICE
+      sub { my($cdr, %opt) = @_; $opt{money_char}. $opt{charge}; },
+
+    ],
+  );
+  $export_formats{'source_default'} = [ 'src', @{ $export_formats{'default'} }, ];
+  $export_formats{'accountcode_default'} =
+    [ @{ $export_formats{'default'} }[0,1],
+      'accountcode',
+      @{ $export_formats{'default'} }[2..5],
+    ];
+
+  %export_formats
+}
 
 sub downstream_csv {
   my( $self, %opt ) = @_;
 
 
 sub downstream_csv {
   my( $self, %opt ) = @_;
 
-  my $format = $opt{'format'}; # 'convergent';
-  return "Unknown format $format" unless exists $export_formats{$format};
+  my $format = $opt{'format'};
+  my %formats = $self->export_formats;
+  return "Unknown format $format" unless exists $formats{$format};
 
   #my $conf = new FS::Conf;
   #$opt{'money_char'} ||= $conf->config('money_char') || '$';
 
   #my $conf = new FS::Conf;
   #$opt{'money_char'} ||= $conf->config('money_char') || '$';
@@ -633,7 +638,7 @@ sub downstream_csv {
     map {
           ref($_) ? &{$_}($self, %opt) : $self->$_();
         }
     map {
           ref($_) ? &{$_}($self, %opt) : $self->$_();
         }
-    @{ $export_formats{$format} };
+    @{ $formats{$format} };
 
   my $status = $csv->combine(@columns);
   die "FS::CDR: error combining ". $csv->error_input(). "into downstream CSV"
 
   my $status = $csv->combine(@columns);
   die "FS::CDR: error combining ". $csv->error_input(). "into downstream CSV"
@@ -769,8 +774,24 @@ sub _cdr_date_parse {
 
   if ( $date =~ /^\s*(\d{4})\D(\d{1,2})\D(\d{1,2})\D+(\d{1,2})\D(\d{1,2})\D(\d{1,2})(\D|$)/ ) {
     ($year, $mon, $day, $hour, $min, $sec) = ( $1, $2, $3, $4, $5, $6 );
 
   if ( $date =~ /^\s*(\d{4})\D(\d{1,2})\D(\d{1,2})\D+(\d{1,2})\D(\d{1,2})\D(\d{1,2})(\D|$)/ ) {
     ($year, $mon, $day, $hour, $min, $sec) = ( $1, $2, $3, $4, $5, $6 );
-  } elsif ( $date  =~ /^\s*(\d{1,2})\D(\d{1,2})\D(\d{4})\s+(\d{1,2})\D(\d{1,2})\D(\d{1,2})(\D|$)/ ) {
+  } elsif ( $date  =~ /^\s*(\d{1,2})\D(\d{1,2})\D(\d{4})\s+(\d{1,2})\D(\d{1,2})(?:\D(\d{1,2}))?(\D|$)/ ) {
+    # 8/26/2010 12:20:01
+    # optionally without seconds
     ($mon, $day, $year, $hour, $min, $sec) = ( $1, $2, $3, $4, $5, $6 );
     ($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+\.\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|$)/ ) {
+    # Taqua OM:  20050422203450943
+    ($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})$/ ) {
+    # 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
+    ($year, $mon, $day, $hour, $min, $sec) = ( $1, $2, $3, $4, $5, $6 );
+    $options{gmt} = 1;
   } else {
      die "unparsable date: $date"; #maybe we shouldn't die...
   }
   } else {
      die "unparsable date: $date"; #maybe we shouldn't die...
   }
@@ -810,7 +831,11 @@ Set true to prevent throwing an error on empty imports
 =cut
 
 my %import_options = (
 =cut
 
 my %import_options = (
-  'table'   => 'cdr',
+  'table'         => 'cdr',
+
+  'batch_keycol'  => 'cdrbatchnum',
+  'batch_table'   => 'cdr_batch',
+  'batch_namecol' => 'cdrbatch',
 
   'formats' => { map { $_ => $cdr_info{$_}->{'import_fields'}; }
                      keys %cdr_info
 
   'formats' => { map { $_ => $cdr_info{$_}->{'import_fields'}; }
                      keys %cdr_info
@@ -833,6 +858,15 @@ my %import_options = (
     { map { $_ => $cdr_info{$_}->{'fixedlength_format'}; }
           keys %cdr_info
     },
     { map { $_ => $cdr_info{$_}->{'fixedlength_format'}; }
           keys %cdr_info
     },
+
+  'format_xml_formats' =>
+    { map { $_ => $cdr_info{$_}->{'xml_format'}; }
+          keys %cdr_info
+    },
+
+  'format_row_callbacks' => { map { $_ => $cdr_info{$_}->{'row_callback'}; }
+                                  keys %cdr_info
+                            },
 );
 
 sub _import_options {
 );
 
 sub _import_options {
@@ -857,7 +891,7 @@ sub process_batch_import {
   my $job = shift;
 
   my $opt = _import_options;
   my $job = shift;
 
   my $opt = _import_options;
-  $opt->{'params'} = [ 'format', 'cdrbatch' ];
+#  $opt->{'params'} = [ 'format', 'cdrbatch' ];
 
   FS::Record::process_batch_import( $job, $opt, @_ );
 
 
   FS::Record::process_batch_import( $job, $opt, @_ );
 
@@ -866,6 +900,42 @@ 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;
+  }
+
+}
+
 =back
 
 =head1 BUGS
 =back
 
 =head1 BUGS