8 use FS::Record qw( qsearch qsearchs );
12 use FS::cdr_upstream_rate;
14 @ISA = qw(FS::Record);
18 FS::cdr - Object methods for cdr records
24 $record = new FS::cdr \%hash;
25 $record = new FS::cdr { 'column' => 'value' };
27 $error = $record->insert;
29 $error = $new_record->replace($old_record);
31 $error = $record->delete;
33 $error = $record->check;
37 An FS::cdr object represents an Call Data Record, typically from a telephony
38 system or provider of some sort. FS::cdr inherits from FS::Record. The
39 following fields are currently supported:
43 =item acctid - primary key
45 =item calldate - Call timestamp (SQL timestamp)
47 =item clid - Caller*ID with text
49 =item src - Caller*ID number / Source number
51 =item dst - Destination extension
53 =item dcontext - Destination context
55 =item channel - Channel used
57 =item dstchannel - Destination channel if appropriate
59 =item lastapp - Last application if appropriate
61 =item lastdata - Last application data
63 =item startdate - Start of call (UNIX-style integer timestamp)
65 =item answerdate - Answer time of call (UNIX-style integer timestamp)
67 =item enddate - End time of call (UNIX-style integer timestamp)
69 =item duration - Total time in system, in seconds
71 =item billsec - Total time call is up, in seconds
73 =item disposition - What happened to the call: ANSWERED, NO ANSWER, BUSY
75 =item amaflags - What flags to use: BILL, IGNORE etc, specified on a per channel basis like accountcode.
79 #ignore the "omit" and "documentation" AMAs??
80 #AMA = Automated Message Accounting.
81 #default: Sets the system default.
82 #omit: Do not record calls.
83 #billing: Mark the entry for billing
84 #documentation: Mark the entry for documentation.
88 =item accountcode - CDR account number to use: account
90 =item uniqueid - Unique channel identifier (Unitel/RSLCOM Event ID)
92 =item userfield - CDR user-defined field
94 =item cdr_type - CDR type - see L<FS::cdr_type> (Usage = 1, S&E = 7, OC&C = 8)
96 =item charged_party - Service number to be billed
98 =item upstream_currency - Wholesale currency from upstream
100 =item upstream_price - Wholesale price from upstream
102 =item upstream_rateplanid - Upstream rate plan ID
104 =item rated_price - Rated (or re-rated) price
106 =item distance - km (need units field?)
108 =item islocal - Local - 1, Non Local = 0
110 =item calltypenum - Type of call - see L<FS::cdr_calltype>
112 =item description - Description (cdr_type 7&8 only) (used for cust_bill_pkg.itemdesc)
114 =item quantity - Number of items (cdr_type 7&8 only)
116 =item carrierid - Upstream Carrier ID (see L<FS::cdr_carrier>)
120 #Telstra =1, Optus = 2, RSL COM = 3
124 =item upstream_rateid - Upstream Rate ID
126 =item svcnum - Link to customer service (see L<FS::cust_svc>)
128 =item freesidestatus - NULL, done (or something)
138 Creates a new CDR. To add the CDR to the database, see L<"insert">.
140 Note that this stores the hash reference, not a distinct copy of the hash it
141 points to. You can ask the object for a copy with the I<hash> method.
145 # the new method can be inherited from FS::Record, if a table method is defined
151 Adds this record to the database. If there is an error, returns the error,
152 otherwise returns false.
156 # the insert method can be inherited from FS::Record
160 Delete this record from the database.
164 # the delete method can be inherited from FS::Record
166 =item replace OLD_RECORD
168 Replaces the OLD_RECORD with this one in the database. If there is an error,
169 returns the error, otherwise returns false.
173 # the replace method can be inherited from FS::Record
177 Checks all fields to make sure this is a valid CDR. If there is
178 an error, returns the error, otherwise returns false. Called by the insert
181 Note: Unlike most types of records, we don't want to "reject" a CDR and we want
182 to process them as quickly as possible, so we allow the database to check most
190 # we don't want to "reject" a CDR like other sorts of input...
192 # $self->ut_numbern('acctid')
193 ## || $self->ut_('calldate')
194 # || $self->ut_text('clid')
195 # || $self->ut_text('src')
196 # || $self->ut_text('dst')
197 # || $self->ut_text('dcontext')
198 # || $self->ut_text('channel')
199 # || $self->ut_text('dstchannel')
200 # || $self->ut_text('lastapp')
201 # || $self->ut_text('lastdata')
202 # || $self->ut_numbern('startdate')
203 # || $self->ut_numbern('answerdate')
204 # || $self->ut_numbern('enddate')
205 # || $self->ut_number('duration')
206 # || $self->ut_number('billsec')
207 # || $self->ut_text('disposition')
208 # || $self->ut_number('amaflags')
209 # || $self->ut_text('accountcode')
210 # || $self->ut_text('uniqueid')
211 # || $self->ut_text('userfield')
212 # || $self->ut_numbern('cdrtypenum')
213 # || $self->ut_textn('charged_party')
214 ## || $self->ut_n('upstream_currency')
215 ## || $self->ut_n('upstream_price')
216 # || $self->ut_numbern('upstream_rateplanid')
217 ## || $self->ut_n('distance')
218 # || $self->ut_numbern('islocal')
219 # || $self->ut_numbern('calltypenum')
220 # || $self->ut_textn('description')
221 # || $self->ut_numbern('quantity')
222 # || $self->ut_numbern('carrierid')
223 # || $self->ut_numbern('upstream_rateid')
224 # || $self->ut_numbern('svcnum')
225 # || $self->ut_textn('freesidestatus')
227 # return $error if $error;
229 #check the foreign keys even?
230 #do we want to outright *reject* the CDR?
232 $self->ut_numbern('acctid')
234 #Usage = 1, S&E = 7, OC&C = 8
235 || $self->ut_foreign_keyn('cdrtypenum', 'cdr_type', 'cdrtypenum' )
237 #the big list in appendix 2
238 || $self->ut_foreign_keyn('calltypenum', 'cdr_calltype', 'calltypenum' )
240 # Telstra =1, Optus = 2, RSL COM = 3
241 || $self->ut_foreign_keyn('carrierid', 'cdr_carrier', 'carrierid' )
243 return $error if $error;
248 =item set_status_and_rated_price STATUS [ RATED_PRICE ]
250 Sets the status to the provided string. If there is an error, returns the
251 error, otherwise returns false.
255 sub set_status_and_rated_price {
256 my($self, $status, $rated_price) = @_;
257 $self->status($status);
258 $self->rated_price($rated_price);
264 Parses the calldate in SQL string format and returns a UNIX timestamp.
269 str2time(shift->calldate);
274 Returns the FS::cdr_carrier object associated with this CDR, or false if no
275 carrierid is defined.
279 my %carrier_cache = ();
283 return '' unless $self->carrierid;
284 $carrier_cache{$self->carrierid} ||=
285 qsearchs('cdr_carrier', { 'carrierid' => $self->carrierid } );
290 Returns the carrier name (see L<FS::cdr_carrier>), or the empty string if
291 no FS::cdr_carrier object is assocated with this CDR.
297 my $cdr_carrier = $self->cdr_carrier;
298 $cdr_carrier ? $cdr_carrier->carriername : '';
303 Returns the FS::cdr_calltype object associated with this CDR, or false if no
304 calltypenum is defined.
308 my %calltype_cache = ();
312 return '' unless $self->calltypenum;
313 $calltype_cache{$self->calltypenum} ||=
314 qsearchs('cdr_calltype', { 'calltypenum' => $self->calltypenum } );
319 Returns the call type name (see L<FS::cdr_calltype>), or the empty string if
320 no FS::cdr_calltype object is assocated with this CDR.
326 my $cdr_calltype = $self->cdr_calltype;
327 $cdr_calltype ? $cdr_calltype->calltypename : '';
330 =item cdr_upstream_rate
332 Returns the upstream rate mapping (see L<FS::cdr_upstream_rate>), or the empty
333 string if no FS::cdr_upstream_rate object is associated with this CDR.
337 sub cdr_upstream_rate {
339 return '' unless $self->upstream_rateid;
340 qsearchs('cdr_upstream_rate', { 'upstream_rateid' => $self->upstream_rateid })
344 =item _convergent_format COLUMN [ COUNTRYCODE ]
346 Returns the number in COLUMN formatted as follows:
348 If the country code does not match COUNTRYCODE (default "61"), it is returned
351 If the country code does match COUNTRYCODE (default "61"), it is removed. In
352 addiiton, "0" is prepended unless the number starts with 13, 18 or 19. (???)
356 sub _convergent_format {
357 my( $self, $field ) = ( shift, shift );
358 my $countrycode = scalar(@_) ? shift : '61'; #+61 = australia
359 #my $number = $self->$field();
360 my $number = $self->get($field);
361 #if ( $number =~ s/^(\+|011)$countrycode// ) {
362 if ( $number =~ s/^\+$countrycode// ) {
364 unless $number =~ /^1[389]/; #???
369 =item downstream_csv [ OPTION => VALUE, ... ]
373 my %export_formats = (
375 'carriername', #CARRIER
376 sub { shift->_convergent_format('src') }, #SERVICE_NUMBER
377 sub { shift->_convergent_format('charged_party') }, #CHARGED_NUMBER
378 sub { time2str('%Y-%m-%d', shift->calldate_unix ) }, #DATE
379 sub { time2str('%T', shift->calldate_unix ) }, #TIME
380 'billsec', #'duration', #DURATION
381 sub { shift->_convergent_format('dst') }, #NUMBER_DIALED
382 '', #XXX add (from prefixes in most recent email) #FROM_DESC
383 '', #XXX add (from prefixes in most recent email) #TO_DESC
384 'calltypename', #CLASS_CODE
385 'rated_price', #PRICE
386 sub { shift->rated_price ? 'Y' : 'N' }, #RATED
392 my( $self, %opt ) = @_;
394 my $format = $opt{'format'}; # 'convergent';
395 return "Unknown format $format" unless exists $export_formats{$format};
397 eval "use Text::CSV_XS;";
399 my $csv = new Text::CSV_XS;
403 ref($_) ? &{$_}($self) : $self->$_();
405 @{ $export_formats{$format} };
407 my $status = $csv->combine(@columns);
408 die "FS::CDR: error combining ". $csv->error_input(). "into downstream CSV"
425 my %import_formats = (
436 'startdate', # XXX will need massaging
450 'calldate', # may need massaging? huh maybe not...
451 #'billsec', #XXX duration and billsec?
452 sub { $_[0]->billsec( $_[1] );
453 $_[0]->duration( $_[1] );
456 'dst', # XXX needs to have "+61" prepended unless /^\+/ ???
460 'upstream_rateplanid',
464 'startdate', #XXX needs massaging
476 my $fh = $param->{filehandle};
477 my $format = $param->{format};
479 return "Unknown format $format" unless exists $import_formats{$format};
481 eval "use Text::CSV_XS;";
484 my $csv = new Text::CSV_XS;
489 local $SIG{HUP} = 'IGNORE';
490 local $SIG{INT} = 'IGNORE';
491 local $SIG{QUIT} = 'IGNORE';
492 local $SIG{TERM} = 'IGNORE';
493 local $SIG{TSTP} = 'IGNORE';
494 local $SIG{PIPE} = 'IGNORE';
496 my $oldAutoCommit = $FS::UID::AutoCommit;
497 local $FS::UID::AutoCommit = 0;
501 while ( defined($line=<$fh>) ) {
503 $csv->parse($line) or do {
504 $dbh->rollback if $oldAutoCommit;
505 return "can't parse: ". $csv->error_input();
508 my @columns = $csv->fields();
509 #warn join('-',@columns);
515 my $field_or_sub = $_;
516 if ( ref($field_or_sub) ) {
517 push @later, $field_or_sub, shift(@columns);
520 ( $field_or_sub => shift @columns );
524 @{ $import_formats{$format} }
527 my $cdr = new FS::cdr ( \%cdr );
529 while ( scalar(@later) ) {
530 my $sub = shift @later;
531 my $data = shift @later;
532 &{$sub}($cdr, $data); # $cdr->&{$sub}($data);
535 my $error = $cdr->insert;
537 $dbh->rollback if $oldAutoCommit;
547 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
549 #might want to disable this if we skip records for any reason...
550 return "Empty file!" unless $imported;
562 L<FS::Record>, schema.html from the base documentation.