taqua vs broadsoft duplicate skipping, RT#86028
[freeside.git] / FS / bin / freeside-cdrrewrited
1 #!/usr/bin/perl -w
2
3 use strict;
4 use vars qw( $conf );
5 use FS::Daemon ':all'; #daemonize1 drop_root daemonize2 myexit logfile sig*
6 use FS::UID qw( adminsuidsetup );
7 use FS::Record qw( qsearch qsearchs dbh );
8 #use FS::cdr;
9 #use FS::cust_pkg;
10 #use FS::queue;
11
12 my $user = shift or die &usage;
13
14 #daemonize1('freeside-sprepaidd', $user); #keep unique pid files w/multi installs
15 daemonize1('freeside-cdrrewrited');
16
17 drop_root();
18
19 adminsuidsetup($user);
20
21 logfile( "%%%FREESIDE_LOG%%%/cdrrewrited-log.". $FS::UID::datasrc );
22
23 daemonize2();
24
25 $conf = new FS::Conf;
26
27 die "not running; relevant conf options are all off\n"
28   unless _shouldrun();
29
30 #--
31
32 #used for taqua
33 my %sessionnum_unmatch = ();
34 my $sessionnum_retry = 4 * 60 * 60; # 4 hours
35 my $sessionnum_giveup = 4 * 24 * 60 * 60; # 4 days
36
37 my %cdr_type = map { lc($_->cdrtypename) => $_->cdrtypenum } 
38   qsearch('cdr_type',{});
39
40 while (1) {
41
42   #hmm... don't want to do an expensive search with an ever-growing bunch
43   # of unprocessed CDRs during the month... better to mark them all as
44   # rewritten "skipped", i.e. why we're a daemon in the first place
45   # instead of just doing this search like normal CDRs
46
47   #hmm :/
48   #used only by taqua, should have no effect otherwise
49   my @recent = grep { ($sessionnum_unmatch{$_} + $sessionnum_retry) > time }
50                  keys %sessionnum_unmatch;
51   my $extra_sql = scalar(@recent)
52                     ? ' AND acctid NOT IN ('. join(',', @recent). ') '
53                     : '';
54
55   #order matters for removing dupes--only the first is preserved
56   $extra_sql .= ' ORDER BY acctid '
57     if $conf->exists('cdr-skip_duplicate_rewrite')
58     || $conf->exists('cdr-skip_duplicate_rewrite-sipcallid');
59
60   my $found = 0;
61   my %skip = (); #used only by taqua
62   my %warning = ();
63
64   foreach my $cdr ( 
65     qsearch( {
66       'table'     => 'cdr',
67       'extra_sql' => 'FOR UPDATE', #XXX overwritten by opt below...would fixing this break anything?
68       'hashref'   => {},
69       'extra_sql' => 'WHERE freesidestatus IS NULL '.
70                      ' AND freesiderewritestatus IS NULL '.
71                      $extra_sql.
72                      ' LIMIT 1024', #arbitrary, but don't eat too much memory
73     } )
74   ) {
75
76     next if $skip{$cdr->acctid}; #used only by taqua
77
78     $found = 1;
79     my @status = ();
80
81     if ($conf->exists('cdr-skip_duplicate_rewrite')) {
82       #qsearch can't handle timestamp type of calldate
83       my $sth = dbh->prepare(
84         'SELECT 1 FROM cdr WHERE src=? AND dst=? AND calldate=? AND acctid < ? LIMIT 1'
85       ) or die dbh->errstr;
86       $sth->execute($cdr->src,$cdr->dst,$cdr->calldate,$cdr->acctid) or die $sth->errstr;
87       my $isdup = $sth->fetchrow_hashref;
88       $sth->finish;
89       if ($isdup) {
90         #we only act on this cdr, not touching previous dupes
91         #if a dupe somehow creeped in previously, too late to fix it
92         $cdr->freesidestatus('skipped'); #prevent it from being billed
93         push(@status,'duplicate');
94       }
95     }
96
97     if ($conf->exists('cdr-skip_duplicate_rewrite-sipcallid')) {
98       my $sth = dbh->prepare(
99         'SELECT 1 FROM cdr WHERE sipcallid=? AND acctid < ? LIMIT 1'
100       ) or die dbh->errstr;
101       $sth->execute($cdr->sipcallid, $cdr->acctid) or die $sth->errstr;
102       my $isdup = $sth->fetchrow_hashref;
103       $sth->finish;
104       if ($isdup) {
105         #we only act on this cdr, not touching previous dupes
106         #if a dupe somehow creeped in previously, too late to fix it
107         $cdr->freesidestatus('skipped'); #prevent it from being billed
108         push(@status,'duplicate');
109       }
110     }
111
112
113     if ( $conf->exists('cdr-asterisk_forward_rewrite')
114          && $cdr->dstchannel =~ /^Local\/(\d+)/i && $1 ne $cdr->dst
115        )
116     {
117
118       my $dst = $1;
119
120       warn "dst ". $cdr->dst. " does not match dstchannel $dst ".
121            "(". $cdr->dstchannel. "); rewriting CDR as a forwarded call";
122
123       $cdr->charged_party($cdr->dst);
124       $cdr->dst($dst);
125       $cdr->amaflags(2);
126
127       push @status, 'asterisk_forward';
128
129     }
130
131     # XXX weird special case stuff--can we modularize this somehow?
132     # reference RT#16271
133     if ( $conf->exists('cdr-asterisk_australia_rewrite') and
134          $cdr->disposition eq 'ANSWERED' ) {
135       my $dst = $cdr->dst;
136       my $type;
137       if ( $dst =~ /^0?(12|13|1800|1900|0055)/ ) {
138         # toll free or smart numbers, any length
139         $type = 'tollfree';
140         $cdr->charged_party($dst);
141       }
142       elsif ( $dst =~ /^(11|0011)/ ) {
143         # will be followed by country code
144         $type = 'international';
145         $dst =~ s/^$1/0011/; #standardize
146         $cdr->dst($dst);
147       }
148       elsif ( length($dst) == 10 and$dst =~ /^04/ ) {
149         $type = 'mobile';
150       }
151       elsif ( length($dst) == 10 and $dst =~ /^02|03|07|08/ ) {
152         $type = 'domestic';
153       }
154       elsif ( length($dst) == 8 ) {
155         # local call, no area code
156         $type = 'domestic';
157       }
158       else {
159         $type = 'other';
160       }
161       if ( $type and exists($cdr_type{$type}) ) {
162         $cdr->cdrtypenum($cdr_type{$type});
163         push @status, 'asterisk_australia';
164       }
165       else {
166         $warning{"no CDR type defined for $type calls"}++;
167       }
168     }
169
170     if ( $conf->exists('cdr-charged_party_rewrite') && ! $cdr->charged_party ) {
171
172       $cdr->set_charged_party;
173       push @status, 'charged_party';
174
175     }
176
177     if (     $cdr->cdrtypenum == 1
178          and $cdr->lastapp
179          and (
180             $conf->exists('cdr-taqua-accountcode_rewrite') or
181             $conf->exists('cdr-taqua-callerid_rewrite') )
182        )
183     {
184
185       #find the matching CDR
186       my %search = ( 'sessionnum' => $cdr->sessionnum );
187       if ( $cdr->lastapp eq 'acctcode' ) {
188         $search{'src'} = $cdr->subscriber;
189       } elsif ( $cdr->lastapp eq 'CallerId' ) {
190         $search{'dst'} = $cdr->subscriber;
191       }
192       my $primary = qsearchs('cdr', \%search);
193
194       unless ( $primary ) {
195
196         my $cantfind = "can't find primary CDR with session ". $cdr->sessionnum.
197                        ", src ". $cdr->subscriber;
198         if ( $cdr->calldate_unix + $sessionnum_giveup < time ) {
199           warn "ERROR: $cantfind; giving up\n";
200           push @status, 'taqua-sessionnum-NOTFOUND';
201           $cdr->status('done'); #so it doesn't try to rate
202           delete $sessionnum_unmatch{$cdr->acctid}; #so it doesn't suck mem
203         } else {
204           warn "WARNING: $cantfind; will keep trying\n";
205           $sessionnum_unmatch{$cdr->acctid} = time;
206           next;
207         }
208
209       } else {
210
211         if ( $cdr->lastapp eq 'acctcode' ) {
212           # lastdata contains the dialed account code
213           $primary->accountcode( $cdr->lastdata );
214           push @status, 'taqua-accountcode';
215         } elsif ( $cdr->lastapp eq 'CallerId' ) {
216           # lastdata contains "allowed" or "restricted"
217           # or case variants thereof
218           if ( lc($cdr->lastdata) eq 'restricted' ) {
219             $primary->clid( 'PRIVATE' );
220           }
221           push @status, 'taqua-callerid';
222         } else {
223           warn "unknown Taqua service name: ".$cdr->lastapp."\n";
224         }
225         #$primary->freesiderewritestatus( 'taqua-accountcode-primary' );
226         my $error = $primary->replace if $primary->modified;
227         if ( $error ) {
228           warn "WARNING: error rewriting primary CDR (will retry): $error\n";
229           next;
230         }
231         $skip{$primary->acctid} = 1;
232
233         $cdr->status('done'); #so it doesn't try to rate
234
235       }
236
237     }
238
239     if ( $conf->exists('cdr-userfield_dnis_rewrite') and
240          $cdr->userfield =~ /DNIS=(\d+)/ ) {
241       $cdr->dst($1);
242       push @status, 'userfield_dnis';
243     }
244
245     if ( $conf->exists('cdr-intl_to_domestic_rewrite') and
246          $cdr->dst =~ /^(011)(\d{0,7})$/ ) {
247       $cdr->dst($2);
248       push @status, 'intl_to_domestic';
249     }
250
251     $cdr->freesiderewritestatus(
252       scalar(@status) ? join('/', @status) : 'skipped'
253     );
254
255     my $error = $cdr->replace;
256
257     if ( $error ) {
258       warn "WARNING: error rewriting CDR (will retry in 30 seconds):".
259            " $error\n";
260       sleep 30; #i dunno, wait and see if the database comes back?
261     }
262
263     last if sigterm() || sigint();
264
265   }
266
267   foreach (sort keys %warning) {
268     warn "WARNING: $_ (x $warning{$_})\n";
269   }
270   %warning = ();
271
272   myexit() if sigterm() || sigint();
273   #sleep 1 unless $found;
274   sleep 5 unless $found;
275
276 }
277
278 #--
279
280 sub _shouldrun {
281      $conf->exists('cdr-asterisk_forward_rewrite')
282   || $conf->exists('cdr-asterisk_australia_rewrite')
283   || $conf->exists('cdr-charged_party_rewrite')
284   || $conf->exists('cdr-taqua-accountcode_rewrite')
285   || $conf->exists('cdr-taqua-callerid_rewrite')
286   || $conf->exists('cdr-intl_to_domestic_rewrite')
287   || $conf->exists('cdr-userfield_dnis_rewrite')
288   || $conf->exists('cdr-skip_duplicate_rewrite')
289   || $conf->exists('cdr-skip_duplicate_rewrite-sipcallid')
290   || 0
291   ;
292 }
293
294 sub usage { 
295   die "Usage:\n\n  freeside-cdrrewrited user\n";
296 }
297
298 =head1 NAME
299
300 freeside-cdrrewrited - Real-time daemon for CDR rewriting
301
302 =head1 SYNOPSIS
303
304   freeside-cdrrewrited
305
306 =head1 DESCRIPTION
307
308 Runs continuously, searches for CDRs and does forwarded-call rewriting if any
309 of the following config options are enabled:
310
311 =over 4
312
313 =item cdr-skip_duplicate_rewrite
314
315 Marks as 'skipped' (prevents billing for) any CDRs with 
316 a src, dst and calldate identical to an existing CDR
317
318 =item cdr-skip_duplicate_rewrite-sipcallid
319
320 Marks as 'skipped' (prevents billing for) any CDRs with 
321 a sipcallid identical to an existing CDR
322
323 =item cdr-asterisk_australia_rewrite
324
325 Classifies Australian numbers as domestic, mobile, tollfree, international, or
326 "other", and tries to assign a cdrtypenum based on that.
327
328 =item cdr-asterisk_forward_rewrite
329
330 Identifies Asterisk forwarded calls using the 'dstchannel' field. If the
331 dstchannel is "Local/" followed by a number, but the number doesn't match the
332 dst field, the dst field will be rewritten to match.
333
334 =item cdr-charged_party_rewrite
335
336 Calls set_charged_party on all calls.
337
338 =item cdr-taqua-accountcode_rewrite
339
340 =item cdr-taqua-callerid_rewrite
341
342 These actually have the same effect. Taqua uses cdrtypenum = 1 to tag accessory
343 records. They will have "sessionnum" = that of the primary record, and
344 "lastapp" indicating their function:
345
346 - "acctcode": "lastdata" contains the dialed account code. Insert this into the
347 accountcode field of the primary record.
348
349 - "CallerId": "lastdata" contains "allowed" or "restricted". If "restricted"
350 then the clid field of the primary record is set to "PRIVATE".
351
352 =item cdr-intl_to_domestic_rewrite
353
354 Finds records where the destination number has the "011" international prefix,
355 but with seven or fewer digits in the rest of the number, and strips the "011"
356 prefix so that they will be treated as domestic calls. This is very uncommon.
357
358 =head1 SEE ALSO
359
360 =cut
361
362 1;