match CDRs to services by IP address, #16723
[freeside.git] / FS / FS / svc_pbx.pm
1 package FS::svc_pbx;
2
3 use strict;
4 use base qw( FS::svc_External_Common );
5 use FS::Record qw( qsearch qsearchs dbh );
6 use FS::Conf;
7 use FS::cust_svc;
8 use FS::svc_phone;
9 use FS::svc_acct;
10
11 =head1 NAME
12
13 FS::svc_pbx - Object methods for svc_pbx records
14
15 =head1 SYNOPSIS
16
17   use FS::svc_pbx;
18
19   $record = new FS::svc_pbx \%hash;
20   $record = new FS::svc_pbx { 'column' => 'value' };
21
22   $error = $record->insert;
23
24   $error = $new_record->replace($old_record);
25
26   $error = $record->delete;
27
28   $error = $record->check;
29
30   $error = $record->suspend;
31
32   $error = $record->unsuspend;
33
34   $error = $record->cancel;
35
36 =head1 DESCRIPTION
37
38 An FS::svc_pbx object represents a PBX tenant.  FS::svc_pbx inherits from
39 FS::svc_Common.  The following fields are currently supported:
40
41 =over 4
42
43 =item svcnum
44
45 Primary key (assigned automatcially for new accounts)
46
47 =item id
48
49 (Unique?) number of external record
50
51 =item title
52
53 PBX name
54
55 =item max_extensions
56
57 Maximum number of extensions
58
59 =item max_simultaneous
60
61 Maximum number of simultaneous users
62
63 =back
64
65 =head1 METHODS
66
67 =over 4
68
69 =item new HASHREF
70
71 Creates a new PBX tenant.  To add the PBX tenant to the database, see
72 L<"insert">.
73
74 Note that this stores the hash reference, not a distinct copy of the hash it
75 points to.  You can ask the object for a copy with the I<hash> method.
76
77 =cut
78
79 sub table { 'svc_pbx'; }
80
81 sub table_info {
82   {
83     'name' => 'PBX',
84     'name_plural' => 'PBXs',
85     'lcname_plural' => 'PBXs',
86     'longname_plural' => 'PBXs',
87     'sorts' => 'svcnum', # optional sort field (or arrayref of sort fields, main first)
88     'display_weight' => 70,
89     'cancel_weight'  => 90,
90     'fields' => {
91       'id'    => 'ID',
92       'title' => 'Name',
93       'max_extensions' => 'Maximum number of User Extensions',
94       'max_simultaneous' => 'Maximum number of simultaneous users',
95     },
96   };
97 }
98
99 =item search_sql STRING
100
101 Class method which returns an SQL fragment to search for the given string.
102
103 =cut
104
105 #XXX
106 #or something more complicated if necessary
107 #sub search_sql {
108 #  my($class, $string) = @_;
109 #  $class->search_sql_field('title', $string);
110 #}
111
112 =item label
113
114 Returns the title field for this PBX tenant.
115
116 =cut
117
118 sub label {
119   my $self = shift;
120   $self->title;
121 }
122
123 =item insert
124
125 Adds this record to the database.  If there is an error, returns the error,
126 otherwise returns false.
127
128 The additional fields pkgnum and svcpart (see L<FS::cust_svc>) should be 
129 defined.  An FS::cust_svc record will be created and inserted.
130
131 =cut
132
133 sub insert {
134   my $self = shift;
135   my $error;
136
137   $error = $self->SUPER::insert;
138   return $error if $error;
139
140   '';
141 }
142
143 =item delete
144
145 Delete this record from the database.
146
147 =cut
148
149 sub delete {
150   my $self = shift;
151
152   local $SIG{HUP} = 'IGNORE';
153   local $SIG{INT} = 'IGNORE';
154   local $SIG{QUIT} = 'IGNORE';
155   local $SIG{TERM} = 'IGNORE';
156   local $SIG{TSTP} = 'IGNORE';
157   local $SIG{PIPE} = 'IGNORE';
158
159   my $oldAutoCommit = $FS::UID::AutoCommit;
160   local $FS::UID::AutoCommit = 0;
161   my $dbh = dbh;
162
163   foreach my $svc_phone (qsearch('svc_phone', { 'pbxsvc' => $self->svcnum } )) {
164     $svc_phone->pbxsvc('');
165     my $error = $svc_phone->replace;
166     if ( $error ) {
167       $dbh->rollback if $oldAutoCommit;
168       return $error;
169     }
170   }
171
172   foreach my $svc_acct  (qsearch('svc_acct',  { 'pbxsvc' => $self->svcnum } )) {
173     my $error = $svc_acct->delete;
174     if ( $error ) {
175       $dbh->rollback if $oldAutoCommit;
176       return $error;
177     }
178   }
179
180   my $error = $self->SUPER::delete;
181   if ( $error ) {
182     $dbh->rollback if $oldAutoCommit;
183     return $error;
184   }
185
186   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
187   '';
188 }
189
190
191 =item replace OLD_RECORD
192
193 Replaces the OLD_RECORD with this one in the database.  If there is an error,
194 returns the error, otherwise returns false.
195
196 =cut
197
198 #sub replace {
199 #  my ( $new, $old ) = ( shift, shift );
200 #  my $error;
201 #
202 #  $error = $new->SUPER::replace($old);
203 #  return $error if $error;
204 #
205 #  '';
206 #}
207
208 =item suspend
209
210 Called by the suspend method of FS::cust_pkg (see L<FS::cust_pkg>).
211
212 =item unsuspend
213
214 Called by the unsuspend method of FS::cust_pkg (see L<FS::cust_pkg>).
215
216 =item cancel
217
218 Called by the cancel method of FS::cust_pkg (see L<FS::cust_pkg>).
219
220 =item check
221
222 Checks all fields to make sure this is a valid PBX tenant.  If there is
223 an error, returns the error, otherwise returns false.  Called by the insert
224 and repalce methods.
225
226 =cut
227
228 sub check {
229   my $self = shift;
230
231   my $x = $self->setfixed;
232   return $x unless ref($x);
233   my $part_svc = $x;
234
235
236   $self->SUPER::check;
237 }
238
239 sub _check_duplicate {
240   my $self = shift;
241
242   my $conf = new FS::Conf;
243   
244   $self->lock_table;
245
246   foreach my $field ('title', 'id') {
247     my $global_unique = $conf->config("global_unique-pbx_$field");
248     # can be 'disabled', 'enabled', or empty.
249     # if empty, check per exports; if not empty or disabled, check 
250     # globally.
251     next if $global_unique eq 'disabled';
252     my @dup = $self->find_duplicates(
253       ($global_unique ? 'global' : 'export') , $field
254     );
255     next if !@dup;
256     return "duplicate $field '".$self->getfield($field).
257            "': conflicts with svcnum ".$dup[0]->svcnum;
258   }
259   return '';
260 }
261
262 =item get_cdrs
263
264 Returns a set of Call Detail Records (see L<FS::cdr>) associated with this 
265 service.  By default, "associated with" means that the "charged_party" field of
266 the CDR matches the "title" field of the service.
267
268 =over 2
269
270 Accepts the following options:
271
272 =item for_update => 1: SELECT the CDRs "FOR UPDATE".
273
274 =item status => "" (or "done"): Return only CDRs with that processing status.
275
276 =item inbound => 1: No-op for svc_pbx CDR processing.
277
278 =item default_prefix => "XXX": Also accept the phone number of the service prepended 
279 with the chosen prefix.
280
281 =item disable_src => 1: No-op for svc_pbx CDR processing.
282
283 =item by_svcnum => 1: Select CDRs where the svcnum field matches, instead of 
284 title/charged_party.  Normally this field is set after processing.
285
286 =item by_ip_addr => 'src' or 'dst': Select CDRs where the src_ip_addr or 
287 dst_ip_addr field matches title.  In this case, some special logic is applied
288 to allow title to indicate a range of IP addresses.
289
290 =item begin, end: Start and end of date range, as unix timestamp.
291
292 =item cdrtypenum: Only return CDRs with this type number.
293
294 =back
295
296 =cut
297
298 sub get_cdrs {
299   my($self, %options) = @_;
300   my %hash = ();
301   my @where = ();
302
303   my @fields = ( 'charged_party' );
304   $hash{'freesidestatus'} = $options{'status'}
305     if exists($options{'status'});
306
307   if ($options{'cdrtypenum'}) {
308     $hash{'cdrtypenum'} = $options{'cdrtypenum'};
309   }
310
311   my $for_update = $options{'for_update'} ? 'FOR UPDATE' : '';
312
313   if ( $options{'by_svcnum'} ) {
314     $hash{'svcnum'} = $self->svcnum;
315   }
316   elsif ( $options{'by_ip_addr'} =~ /^src|dst$/) {
317     my $field = 'cdr.'.$options{'by_ip_addr'}.'_ip_addr';
318     push @where, FS::cdr->ip_addr_sql($field, $self->title);
319   }
320   else {
321     #matching by title
322     my $title = $self->title;
323
324     my $prefix = $options{'default_prefix'};
325
326     my @orwhere =  map " $_ = '$title'        ", @fields;
327     push @orwhere, map " $_ = '$prefix$title' ", @fields
328       if length($prefix);
329     if ( $prefix =~ /^\+(\d+)$/ ) {
330       push @orwhere, map " $_ = '$1$title' ", @fields
331     }
332
333     push @where, ' ( '. join(' OR ', @orwhere ). ' ) ';
334   }
335
336   if ( $options{'begin'} ) {
337     push @where, 'startdate >= '. $options{'begin'};
338   }
339   if ( $options{'end'} ) {
340     push @where, 'startdate < '.  $options{'end'};
341   }
342
343   my $extra_sql = ( keys(%hash) ? ' AND ' : ' WHERE ' ). join(' AND ', @where )
344     if @where;
345
346   my @cdrs =
347     qsearch( {
348       'table'      => 'cdr',
349       'hashref'    => \%hash,
350       'extra_sql'  => $extra_sql,
351       'order_by'   => "ORDER BY startdate $for_update",
352     } );
353
354   @cdrs;
355 }
356
357 =back
358
359 =head1 BUGS
360
361 =head1 SEE ALSO
362
363 L<FS::svc_Common>, L<FS::Record>, L<FS::cust_svc>, L<FS::part_svc>,
364 L<FS::cust_pkg>, schema.html from the base documentation.
365
366 =cut
367
368 1;
369