default to a session cookie instead of setting an explicit timeout, weird timezone...
[freeside.git] / FS / bin / freeside-cdrrated
1 #!/usr/bin/perl -w
2
3 use strict;
4 use FS::Daemon ':all'; #daemonize1 drop_root daemonize2 myexit logfile sig*
5 use FS::UID qw( adminsuidsetup );
6 use FS::Record qw( qsearch qsearchs );
7 use FS::cdr;
8 use FS::svc_phone;
9 use FS::part_pkg;
10
11 my $user = shift or die &usage;
12
13 daemonize1('freeside-cdrrated');
14
15 drop_root();
16
17 adminsuidsetup($user);
18
19 logfile( "%%%FREESIDE_LOG%%%/cdrrated-log.". $FS::UID::datasrc );
20
21 daemonize2();
22
23 our $conf = new FS::Conf;
24
25 die "not running; cdr-prerate conf option is off\n"
26   unless _shouldrun();
27
28 #--
29
30 my $extra_sql = '';
31 my @cdrtypenums = $conf->config('cdr-prerate-cdrtypenums');
32 if ( @cdrtypenums ) {
33   $extra_sql .= ' AND cdrtypenum IN ('. join(',', @cdrtypenums ). ')';
34 }
35
36 #our %svcnum = ();   # phonenum => svcnum
37 our %svc_phone = (); # phonenum => svc_phone
38 our %pkgnum = ();    # phonenum => pkgnum
39 our %cust_pkg = ();  # pkgnum   => cust_pkg (NOT phonenum => cust_pkg!)
40 our %pkgpart = ();   # phonenum => pkgpart
41 our %part_pkg = ();  # pkgpart  => part_pkg
42
43 #some false laziness w/freeside-cdrrewrited
44
45 while (1) {
46
47   my $found = 0;
48   foreach my $cdr (
49     qsearch( {
50       'table'     => 'cdr',
51       'hashref'   => { 'freesidestatus' => '' },
52       'extra_sql' => $extra_sql.
53                      ' LIMIT 1024'. #arbitrary, but don't eat too much memory
54                      ' FOR UPDATE',
55     } )
56
57   ) {
58
59     $found = 1;
60
61     #find the matching service - some weird false laziness w/svc_phone::get_cdrs
62
63     #in charged_party or src
64     #hmm... edge case; get_cdrs rating will match a src if a charged_party is
65     # present #but doesn't match a service...
66     my $number = $cdr->charged_party || $cdr->src;
67
68     #technically default_prefix. phonenum or phonenum (or default_prefix without the + . phonenum)
69     #but for now we're just assuming default_prefix is +1
70     my $prefix = '+1'; #$options{'default_prefix'};
71
72     $number = substr($number, length($prefix))
73       if $prefix eq substr($number, 0, length($prefix));
74     if ( $prefix && $prefix =~ /^\+(\d+)$/ ) {
75       $prefix = $1;
76       $number = substr($number, length($prefix))
77         if $prefix eq substr($number, 0, length($prefix));
78     }
79
80     unless ( $svc_phone{$number} ) {
81       #only phone number matching supported right now
82       my $svc_phone = qsearchs('svc_phone', { 'phonenum' => $number } );
83       unless ( $svc_phone ) {
84         #XXX set freesideratestatus or something so we don't keep retrying?
85         warn "no phone number found for CDR ". $cdr->acctid. "\n";
86         next;
87       }
88
89       $svc_phone{$number} = $svc_phone;
90
91     }
92
93     unless ( $pkgnum{$number} ) {
94
95       my $cust_pkg = $svc_phone{$number}->cust_svc->cust_pkg;
96       unless ( $cust_pkg ) {
97         #XXX unlinked svc_phone?
98         # also set freesideratestatus or somesuch?
99         warn "no package found (unlinked phone number?) for CDR ". $cdr->acctid. "\n";
100         next;
101       }
102
103       $pkgnum{$number} = $cust_pkg->pkgnum;
104       $cust_pkg{$cust_pkg->pkgnum} ||= $cust_pkg;
105
106     }
107
108     unless ( $pkgpart{$number} ) {
109
110       #get the package, search through the part_pkg and linked for a voip_cdr def w/matching cdrtypenum (or no use_cdrtypenum)
111       my @part_pkg =
112         grep { $_->plan eq 'voip_cdr'
113                  && ( ! length($_->option_cacheable('use_cdrtypenum'))
114                       || $_->option_cacheable('use_cdrtypenum')
115                            eq $cdr->cdrtypenum #eq otherwise 0 matches ''
116                     )
117                  && ( ! length($_->option_cacheable('ignore_cdrtypenum'))
118                       || $_->option_cacheable('ignore_cdrtypenum')
119                            ne $cdr->cdrtypenum #ne otherwise 0 matches ''
120                     )
121
122              }
123           $cust_pkg{ $pkgnum{$number} }->part_pkg->self_and_bill_linked;
124
125       if ( ! @part_pkg ) {
126         #XXX no package for this CDR
127         # warn and also set freesideratestatus or somesuch?
128         #  or at least warn
129         warn "no CDR rating package for CDR ". $cdr->acctid. "\n";
130         next;
131       } elsif ( scalar(@part_pkg) > 1 ) {
132         warn "multiple package could rate CDR ". $cdr->acctid. "\n";
133         # and also set freesideratestatus or somesuch?
134         next;
135       }
136
137       $pkgpart{$number} = $part_pkg[0]->pkgpart;
138       $part_pkg{ $part_pkg[0]->pkgpart } ||= $part_pkg[0];
139
140     } 
141
142     if ( $part_pkg{ $pkgpart{$number} }->option('min_included') ) {
143       #then we can't prerate this CDR
144       #some sort of warning?
145       # (sucks if you're depending on credit limit fraud warnings)
146       warn "package has min_included; can't prerate CDR ". $cdr->acctid. "\n";
147       next;
148     }
149     
150     my $error = $cdr->rate(
151       'part_pkg' => $part_pkg{ $pkgpart{$number} },
152       'cust_pkg' => $cust_pkg{ $pkgnum{$number} },
153       'svcnum'   => $svc_phone{$number}->svcnum,
154     );
155     if ( $error ) {
156       warn "Can't prerate CDR ". $cdr->acctid. ' to '. $cdr->dst. ": $error";
157       #could be an included minutes CDR, so don't sleep 30;
158     } else {
159
160       #this could get expensive on a per-call basis
161       # trigger in a separate process with less frequency?
162       
163       my $cust_main = $cust_pkg{ $pkgnum{$number} }->cust_main;
164
165       my $error = $cust_main->check_credit_limit;
166       if ( $error ) {
167         #"should never happen" normally, but as a daemon, better to survive
168         # e.g. database going away and coming back and resume doing our thing
169         warn $error;
170         sleep 30;
171       }
172
173     }
174
175     last if sigterm() || sigint();
176
177   }
178
179   myexit() if sigterm() || sigint();
180   sleep 5 unless $found;
181
182 }
183
184 #--
185
186 sub _shouldrun {
187   $conf->exists('cdr-prerate');
188 }
189
190 sub usage { 
191   die "Usage:\n\n  freeside-cdrrated user\n";
192 }
193
194 =head1 NAME
195
196 freeside-cdrrated - Real-time daemon for CDR rating
197
198 =head1 SYNOPSIS
199
200   freeside-cdrrated
201
202 =head1 DESCRIPTION
203
204 Runs continuously, searches for CDRs and which can be pre-rated, and rates them.
205
206 =head1 SEE ALSO
207
208 cdr-prerate configuration setting
209
210 =cut
211
212 1;
213