enable CardFortress in test database, #71513
[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
59   my $found = 0;
60   my %skip = (); #used only by taqua
61   my %warning = ();
62
63   foreach my $cdr ( 
64     qsearch( {
65       'table'     => 'cdr',
66       'hashref'   => {},
67       'extra_sql' => 'WHERE freesidestatus IS NULL '.
68                      ' AND freesiderewritestatus IS NULL '.
69                      $extra_sql.
70                      ' LIMIT 1024 FOR UPDATE', #arbitrary, but don't eat too much memory
71     } )
72   ) {
73
74     next if $skip{$cdr->acctid}; #used only by taqua
75
76     $found = 1;
77     my @status = ();
78
79     if ($conf->exists('cdr-skip_duplicate_rewrite')) {
80       #qsearch can't handle timestamp type of calldate
81       my $sth = dbh->prepare(
82         'SELECT 1 FROM cdr WHERE src=? AND dst=? AND calldate=? AND acctid < ? LIMIT 1'
83       ) or die dbh->errstr;
84       $sth->execute($cdr->src,$cdr->dst,$cdr->calldate,$cdr->acctid) or die $sth->errstr;
85       my $isdup = $sth->fetchrow_hashref;
86       $sth->finish;
87       if ($isdup) {
88         #we only act on this cdr, not touching previous dupes
89         #if a dupe somehow creeped in previously, too late to fix it
90         $cdr->freesidestatus('skipped'); #prevent it from being billed
91         push(@status,'duplicate');
92       }
93     }
94
95     if ( $conf->exists('cdr-asterisk_forward_rewrite')
96          && $cdr->dstchannel =~ /^Local\/(\d+)/i && $1 ne $cdr->dst
97        )
98     {
99
100       my $dst = $1;
101
102       warn "dst ". $cdr->dst. " does not match dstchannel $dst ".
103            "(". $cdr->dstchannel. "); rewriting CDR as a forwarded call";
104
105       $cdr->charged_party($cdr->dst);
106       $cdr->dst($dst);
107       $cdr->amaflags(2);
108
109       push @status, 'asterisk_forward';
110
111     }
112
113     # XXX weird special case stuff--can we modularize this somehow?
114     # reference RT#16271
115     if ( $conf->exists('cdr-asterisk_australia_rewrite') and
116          $cdr->disposition eq 'ANSWERED' ) {
117       my $dst = $cdr->dst;
118       my $type;
119       if ( $dst =~ /^0?(12|13|1800|1900|0055)/ ) {
120         # toll free or smart numbers, any length
121         $type = 'tollfree';
122         $cdr->charged_party($dst);
123       }
124       elsif ( $dst =~ /^(11|0011)/ ) {
125         # will be followed by country code
126         $type = 'international';
127         $dst =~ s/^$1/0011/; #standardize
128         $cdr->dst($dst);
129       }
130       elsif ( length($dst) == 10 and$dst =~ /^04/ ) {
131         $type = 'mobile';
132       }
133       elsif ( length($dst) == 10 and $dst =~ /^02|03|07|08/ ) {
134         $type = 'domestic';
135       }
136       elsif ( length($dst) == 8 ) {
137         # local call, no area code
138         $type = 'domestic';
139       }
140       else {
141         $type = 'other';
142       }
143       if ( $type and exists($cdr_type{$type}) ) {
144         $cdr->cdrtypenum($cdr_type{$type});
145         push @status, 'asterisk_australia';
146       }
147       else {
148         $warning{"no CDR type defined for $type calls"}++;
149       }
150     }
151
152     if ( $conf->exists('cdr-charged_party_rewrite') && ! $cdr->charged_party ) {
153
154       $cdr->set_charged_party;
155       push @status, 'charged_party';
156
157     }
158
159     if (     $cdr->cdrtypenum == 1
160          and $cdr->lastapp
161          and (
162             $conf->exists('cdr-taqua-accountcode_rewrite') or
163             $conf->exists('cdr-taqua-callerid_rewrite') )
164        )
165     {
166
167       #find the matching CDR
168       my %search = ( 'sessionnum' => $cdr->sessionnum );
169       if ( $cdr->lastapp eq 'acctcode' ) {
170         $search{'src'} = $cdr->subscriber;
171       } elsif ( $cdr->lastapp eq 'CallerId' ) {
172         $search{'dst'} = $cdr->subscriber;
173       }
174       my $primary = qsearchs('cdr', \%search);
175
176       unless ( $primary ) {
177
178         my $cantfind = "can't find primary CDR with session ". $cdr->sessionnum.
179                        ", src ". $cdr->subscriber;
180         if ( $cdr->calldate_unix + $sessionnum_giveup < time ) {
181           warn "ERROR: $cantfind; giving up\n";
182           push @status, 'taqua-sessionnum-NOTFOUND';
183           $cdr->status('done'); #so it doesn't try to rate
184           delete $sessionnum_unmatch{$cdr->acctid}; #so it doesn't suck mem
185         } else {
186           warn "WARNING: $cantfind; will keep trying\n";
187           $sessionnum_unmatch{$cdr->acctid} = time;
188           next;
189         }
190
191       } else {
192
193         if ( $cdr->lastapp eq 'acctcode' ) {
194           # lastdata contains the dialed account code
195           $primary->accountcode( $cdr->lastdata );
196           push @status, 'taqua-accountcode';
197         } elsif ( $cdr->lastapp eq 'CallerId' ) {
198           # lastdata contains "allowed" or "restricted"
199           # or case variants thereof
200           if ( lc($cdr->lastdata) eq 'restricted' ) {
201             $primary->clid( 'PRIVATE' );
202           }
203           push @status, 'taqua-callerid';
204         } else {
205           warn "unknown Taqua service name: ".$cdr->lastapp."\n";
206         }
207         #$primary->freesiderewritestatus( 'taqua-accountcode-primary' );
208         my $error = $primary->replace if $primary->modified;
209         if ( $error ) {
210           warn "WARNING: error rewriting primary CDR (will retry): $error\n";
211           next;
212         }
213         $skip{$primary->acctid} = 1;
214
215         $cdr->status('done'); #so it doesn't try to rate
216
217       }
218
219     }
220
221     if ( $conf->exists('cdr-userfield_dnis_rewrite') and
222          $cdr->userfield =~ /DNIS=(\d+)/ ) {
223       $cdr->dst($1);
224       push @status, 'userfield_dnis';
225     }
226
227     if ( $conf->exists('cdr-intl_to_domestic_rewrite') and
228          $cdr->dst =~ /^(011)(\d{0,7})$/ ) {
229       $cdr->dst($2);
230       push @status, 'intl_to_domestic';
231     }
232
233     $cdr->freesiderewritestatus(
234       scalar(@status) ? join('/', @status) : 'skipped'
235     );
236
237     my $error = $cdr->replace;
238
239     if ( $error ) {
240       warn "WARNING: error rewriting CDR (will retry in 30 seconds):".
241            " $error\n";
242       sleep 30; #i dunno, wait and see if the database comes back?
243     }
244
245     last if sigterm() || sigint();
246
247   }
248
249   foreach (sort keys %warning) {
250     warn "WARNING: $_ (x $warning{$_})\n";
251   }
252   %warning = ();
253
254   myexit() if sigterm() || sigint();
255   #sleep 1 unless $found;
256   sleep 5 unless $found;
257
258 }
259
260 #--
261
262 sub _shouldrun {
263      $conf->exists('cdr-asterisk_forward_rewrite')
264   || $conf->exists('cdr-asterisk_australia_rewrite')
265   || $conf->exists('cdr-charged_party_rewrite')
266   || $conf->exists('cdr-taqua-accountcode_rewrite')
267   || $conf->exists('cdr-taqua-callerid_rewrite')
268   || $conf->exists('cdr-intl_to_domestic_rewrite')
269   || $conf->exists('cdr-userfield_dnis_rewrite')
270   || $conf->exists('cdr-skip_duplicate_rewrite')
271   || 0
272   ;
273 }
274
275 sub usage { 
276   die "Usage:\n\n  freeside-cdrrewrited user\n";
277 }
278
279 =head1 NAME
280
281 freeside-cdrrewrited - Real-time daemon for CDR rewriting
282
283 =head1 SYNOPSIS
284
285   freeside-cdrrewrited
286
287 =head1 DESCRIPTION
288
289 Runs continuously, searches for CDRs and does forwarded-call rewriting if any
290 of the following config options are enabled:
291
292 =over 4
293
294 =item cdr-skip_duplicate_rewrite
295
296 Marks as 'skipped' (prevents billing for) any CDRs with 
297 a src, dst and calldate identical to an existing CDR
298
299 =item cdr-asterisk_australia_rewrite
300
301 Classifies Australian numbers as domestic, mobile, tollfree, international, or
302 "other", and tries to assign a cdrtypenum based on that.
303
304 =item cdr-asterisk_forward_rewrite
305
306 Identifies Asterisk forwarded calls using the 'dstchannel' field. If the
307 dstchannel is "Local/" followed by a number, but the number doesn't match the
308 dst field, the dst field will be rewritten to match.
309
310 =item cdr-charged_party_rewrite
311
312 Calls set_charged_party on all calls.
313
314 =item cdr-taqua-accountcode_rewrite
315
316 =item cdr-taqua-callerid_rewrite
317
318 These actually have the same effect. Taqua uses cdrtypenum = 1 to tag accessory
319 records. They will have "sessionnum" = that of the primary record, and
320 "lastapp" indicating their function:
321
322 - "acctcode": "lastdata" contains the dialed account code. Insert this into the
323 accountcode field of the primary record.
324
325 - "CallerId": "lastdata" contains "allowed" or "restricted". If "restricted"
326 then the clid field of the primary record is set to "PRIVATE".
327
328 =item cdr-intl_to_domestic_rewrite
329
330 Finds records where the destination number has the "011" international prefix,
331 but with seven or fewer digits in the rest of the number, and strips the "011"
332 prefix so that they will be treated as domestic calls. This is very uncommon.
333
334 =head1 SEE ALSO
335
336 =cut
337
338 1;