CDR schema and class
[freeside.git] / FS / FS / cdr.pm
1 package FS::cdr;
2
3 use strict;
4 use vars qw( @ISA );
5 use Date::Parse;
6 use FS::UID qw( dbh );
7 use FS::Record qw( qsearch qsearchs );
8 use FS::cdr_type;
9 use FS::cdr_calltype;
10 use FS::cdr_carrier;
11
12 @ISA = qw(FS::Record);
13
14 =head1 NAME
15
16 FS::cdr - Object methods for cdr records
17
18 =head1 SYNOPSIS
19
20   use FS::cdr;
21
22   $record = new FS::cdr \%hash;
23   $record = new FS::cdr { 'column' => 'value' };
24
25   $error = $record->insert;
26
27   $error = $new_record->replace($old_record);
28
29   $error = $record->delete;
30
31   $error = $record->check;
32
33 =head1 DESCRIPTION
34
35 An FS::cdr object represents an Call Data Record, typically from a telephony
36 system or provider of some sort.  FS::cdr inherits from FS::Record.  The
37 following fields are currently supported:
38
39 =over 4
40
41 =item acctid - primary key
42
43 =item calldate - Call timestamp (SQL timestamp)
44
45 =item clid - Caller*ID with text
46
47 =item src - Caller*ID number / Source number
48
49 =item dst - Destination extension
50
51 =item dcontext - Destination context
52
53 =item channel - Channel used
54
55 =item dstchannel - Destination channel if appropriate
56
57 =item lastapp - Last application if appropriate
58
59 =item lastdata - Last application data
60
61 =item startdate - Start of call (UNIX-style integer timestamp)
62
63 =item answerdate - Answer time of call (UNIX-style integer timestamp)
64
65 =item enddate - End time of call (UNIX-style integer timestamp)
66
67 =item duration - Total time in system, in seconds
68
69 =item billsec - Total time call is up, in seconds
70
71 =item disposition - What happened to the call: ANSWERED, NO ANSWER, BUSY 
72
73 =item amaflags - What flags to use: BILL, IGNORE etc, specified on a per channel basis like accountcode. 
74
75 =cut
76
77   #ignore the "omit" and "documentation" AMAs??
78   #AMA = Automated Message Accounting. 
79   #default: Sets the system default. 
80   #omit: Do not record calls. 
81   #billing: Mark the entry for billing 
82   #documentation: Mark the entry for documentation.
83
84 =back
85
86 =item accountcode - CDR account number to use: account
87
88 =item uniqueid - Unique channel identifier (Unitel/RSLCOM Event ID)
89
90 =item userfield - CDR user-defined field
91
92 =item cdr_type - CDR type - see L<FS::cdr_type> (Usage = 1, S&E = 7, OC&C = 8)
93
94 =item charged_party - Service number to be billed
95
96 =item upstream_currency - Wholesale currency from upstream
97
98 =item upstream_price - Wholesale price from upstream
99
100 =item upstream_rateplanid - Upstream rate plan ID
101
102 =item distance - km (need units field?)
103
104 =item islocal - Local - 1, Non Local = 0
105
106 =item calltypenum - Type of call - see L<FS::cdr_calltype>
107
108 =item description - Description (cdr_type 7&8 only) (used for cust_bill_pkg.itemdesc)
109
110 =item quantity - Number of items (cdr_type 7&8 only)
111
112 =item carrierid - Upstream Carrier ID (see L<FS::cdr_carrier>) 
113
114 =cut
115
116 #Telstra =1, Optus = 2, RSL COM = 3
117
118 =back
119
120 =item upstream_rateid - Upstream Rate ID
121
122 =item svcnum - Link to customer service (see L<FS::cust_svc>)
123
124 =item freesidestatus - NULL, done, skipped, pushed_downstream (or something)
125
126 =back
127
128 =head1 METHODS
129
130 =over 4
131
132 =item new HASHREF
133
134 Creates a new CDR.  To add the CDR to the database, see L<"insert">.
135
136 Note that this stores the hash reference, not a distinct copy of the hash it
137 points to.  You can ask the object for a copy with the I<hash> method.
138
139 =cut
140
141 # the new method can be inherited from FS::Record, if a table method is defined
142
143 sub table { 'cdr'; }
144
145 =item insert
146
147 Adds this record to the database.  If there is an error, returns the error,
148 otherwise returns false.
149
150 =cut
151
152 # the insert method can be inherited from FS::Record
153
154 =item delete
155
156 Delete this record from the database.
157
158 =cut
159
160 # the delete method can be inherited from FS::Record
161
162 =item replace OLD_RECORD
163
164 Replaces the OLD_RECORD with this one in the database.  If there is an error,
165 returns the error, otherwise returns false.
166
167 =cut
168
169 # the replace method can be inherited from FS::Record
170
171 =item check
172
173 Checks all fields to make sure this is a valid CDR.  If there is
174 an error, returns the error, otherwise returns false.  Called by the insert
175 and replace methods.
176
177 Note: Unlike most types of records, we don't want to "reject" a CDR and we want
178 to process them as quickly as possible, so we allow the database to check most
179 of the data.
180
181 =cut
182
183 sub check {
184   my $self = shift;
185
186 # we don't want to "reject" a CDR like other sorts of input...
187 #  my $error = 
188 #    $self->ut_numbern('acctid')
189 ##    || $self->ut_('calldate')
190 #    || $self->ut_text('clid')
191 #    || $self->ut_text('src')
192 #    || $self->ut_text('dst')
193 #    || $self->ut_text('dcontext')
194 #    || $self->ut_text('channel')
195 #    || $self->ut_text('dstchannel')
196 #    || $self->ut_text('lastapp')
197 #    || $self->ut_text('lastdata')
198 #    || $self->ut_numbern('startdate')
199 #    || $self->ut_numbern('answerdate')
200 #    || $self->ut_numbern('enddate')
201 #    || $self->ut_number('duration')
202 #    || $self->ut_number('billsec')
203 #    || $self->ut_text('disposition')
204 #    || $self->ut_number('amaflags')
205 #    || $self->ut_text('accountcode')
206 #    || $self->ut_text('uniqueid')
207 #    || $self->ut_text('userfield')
208 #    || $self->ut_numbern('cdrtypenum')
209 #    || $self->ut_textn('charged_party')
210 ##    || $self->ut_n('upstream_currency')
211 ##    || $self->ut_n('upstream_price')
212 #    || $self->ut_numbern('upstream_rateplanid')
213 ##    || $self->ut_n('distance')
214 #    || $self->ut_numbern('islocal')
215 #    || $self->ut_numbern('calltypenum')
216 #    || $self->ut_textn('description')
217 #    || $self->ut_numbern('quantity')
218 #    || $self->ut_numbern('carrierid')
219 #    || $self->ut_numbern('upstream_rateid')
220 #    || $self->ut_numbern('svcnum')
221 #    || $self->ut_textn('freesidestatus')
222 #  ;
223 #  return $error if $error;
224
225   #check the foreign keys even?
226   #do we want to outright *reject* the CDR?
227   my $error =
228        $self->ut_numbern('acctid')
229
230     #Usage = 1, S&E = 7, OC&C = 8
231     || $self->ut_foreign_keyn('cdrtypenum',  'cdr_type',     'cdrtypenum' )
232
233     #the big list in appendix 2
234     || $self->ut_foreign_keyn('calltypenum', 'cdr_calltype', 'calltypenum' )
235
236     # Telstra =1, Optus = 2, RSL COM = 3
237     || $self->ut_foreign_keyn('carrierid', 'cdr_carrier', 'carrierid' )
238   ;
239   return $error if $error;
240
241   $self->SUPER::check;
242 }
243
244 my %formats = (
245   'asterisk' => [
246     'accountcode',
247     'src',
248     'dst',
249     'dcontext',
250     'clid',
251     'channel',
252     'dstchannel',
253     'lastapp',
254     'lastdata',
255     'startdate', # XXX will need massaging
256     'answer',    # XXX same
257     'end',       # XXX same
258     'duration',
259     'billsec',
260     'disposition',
261     'amaflags',
262     'uniqueid',
263     'userfield',
264   ],
265   'unitel' => [
266     'uniqueid',
267     'cdr_type',
268     'calldate', # XXX may need massaging
269     'billsec', #XXX duration and billsec?
270                # sub { $_[0]->billsec(  $_[1] );
271                #       $_[0]->duration( $_[1] );
272                #     },
273     'src',
274     'dst',
275     'charged_party',
276     'upstream_currency',
277     'upstream_price',
278     'upstream_rateplanid',
279     'distance',
280     'islocal',
281     'calltypenum',
282     'startdate', # XXX will definitely need massaging
283     'enddate',   # XXX same
284     'description',
285     'quantity',
286     'carrierid',
287     'upstream_rateid',
288   ]
289 );
290
291 sub batch_import {
292   my $param = shift;
293
294   my $fh = $param->{filehandle};
295   my $format = $param->{format};
296
297   return "Unknown format $format" unless exists $formats{$format};
298
299   eval "use Text::CSV_XS;";
300   die $@ if $@;
301
302   my $csv = new Text::CSV_XS;
303
304   my $imported = 0;
305   #my $columns;
306
307   local $SIG{HUP} = 'IGNORE';
308   local $SIG{INT} = 'IGNORE';
309   local $SIG{QUIT} = 'IGNORE';
310   local $SIG{TERM} = 'IGNORE';
311   local $SIG{TSTP} = 'IGNORE';
312   local $SIG{PIPE} = 'IGNORE';
313
314   my $oldAutoCommit = $FS::UID::AutoCommit;
315   local $FS::UID::AutoCommit = 0;
316   my $dbh = dbh;
317   
318   my $line;
319   while ( defined($line=<$fh>) ) {
320
321     $csv->parse($line) or do {
322       $dbh->rollback if $oldAutoCommit;
323       return "can't parse: ". $csv->error_input();
324     };
325
326     my @columns = $csv->fields();
327     #warn join('-',@columns);
328
329     my @later = ();
330     my %cdr =
331       map {
332
333         my $field_or_sub = $_;
334         if ( ref($field_or_sub) ) {
335           push @later, $field_or_sub, shift(@columns);
336           ();
337         } else {
338           ( $field_or_sub => shift @columns );
339         }
340
341       }
342       @{ $formats{$format} }
343     ;
344
345     my $cdr = new FS::cdr ( \%cdr );
346
347     while ( scalar(@later) ) {
348       my $sub = shift @later;
349       my $data = shift @later;
350       &{$sub}($cdr, $data);  # $cdr->&{$sub}($data); 
351     }
352
353     my $error = $cdr->insert;
354     if ( $error ) {
355       $dbh->rollback if $oldAutoCommit;
356       return $error;
357
358       #or just skip?
359       #next;
360     }
361
362     $imported++;
363   }
364
365   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
366
367   #might want to disable this if we skip records for any reason...
368   return "Empty file!" unless $imported;
369
370   '';
371
372 }
373
374 =back
375
376 =head1 BUGS
377
378 =head1 SEE ALSO
379
380 L<FS::Record>, schema.html from the base documentation.
381
382 =cut
383
384 1;
385