finishing e911/svc_phone location, RT#7047
[freeside.git] / FS / FS / svc_phone.pm
1 package FS::svc_phone;
2
3 use strict;
4 use base qw( FS::svc_Domain_Mixin FS::location_Mixin FS::svc_Common );
5 use vars qw( $DEBUG $me @pw_set $conf );
6 use Data::Dumper;
7 use Scalar::Util qw( blessed );
8 use FS::Conf;
9 use FS::Record qw( qsearch qsearchs dbh );
10 use FS::Msgcat qw(gettext);
11 use FS::part_svc;
12 use FS::phone_device;
13 use FS::svc_pbx;
14 use FS::svc_domain;
15 use FS::cust_location;
16
17 $me = '[' . __PACKAGE__ . ']';
18 $DEBUG = 0;
19
20 #avoid l 1 and o O 0
21 @pw_set = ( 'a'..'k', 'm','n', 'p-z', 'A'..'N', 'P'..'Z' , '2'..'9' );
22
23 #ask FS::UID to run this stuff for us later
24 $FS::UID::callback{'FS::svc_acct'} = sub { 
25   $conf = new FS::Conf;
26 };
27
28 =head1 NAME
29
30 FS::svc_phone - Object methods for svc_phone records
31
32 =head1 SYNOPSIS
33
34   use FS::svc_phone;
35
36   $record = new FS::svc_phone \%hash;
37   $record = new FS::svc_phone { 'column' => 'value' };
38
39   $error = $record->insert;
40
41   $error = $new_record->replace($old_record);
42
43   $error = $record->delete;
44
45   $error = $record->check;
46
47   $error = $record->suspend;
48
49   $error = $record->unsuspend;
50
51   $error = $record->cancel;
52
53 =head1 DESCRIPTION
54
55 An FS::svc_phone object represents a phone number.  FS::svc_phone inherits
56 from FS::Record.  The following fields are currently supported:
57
58 =over 4
59
60 =item svcnum
61
62 primary key
63
64 =item countrycode
65
66 =item phonenum
67
68 =item sip_password
69
70 =item pin
71
72 Voicemail PIN
73
74 =item phone_name
75
76 =item pbxsvc
77
78 Optional svcnum from svc_pbx
79
80 =back
81
82 =head1 METHODS
83
84 =over 4
85
86 =item new HASHREF
87
88 Creates a new phone number.  To add the number to the database, see L<"insert">.
89
90 Note that this stores the hash reference, not a distinct copy of the hash it
91 points to.  You can ask the object for a copy with the I<hash> method.
92
93 =cut
94
95 # the new method can be inherited from FS::Record, if a table method is defined
96 #
97 sub table_info {
98   {
99     'name' => 'Phone number',
100     'sorts' => 'phonenum',
101     'display_weight' => 60,
102     'cancel_weight'  => 80,
103     'fields' => {
104         'countrycode'  => { label => 'Country code',
105                             type  => 'text',
106                             disable_inventory => 1,
107                             disable_select => 1,
108                           },
109         'phonenum'     => 'Phone number',
110         'pin'          => { label => 'Personal Identification Number',
111                             type  => 'text',
112                             disable_inventory => 1,
113                             disable_select => 1,
114                           },
115         'sip_password' => 'SIP password',
116         'phone_name'   => 'Name',
117         'pbxsvc'       => { label => 'PBX',
118                             type  => 'select-svc_pbx.html',
119                             disable_inventory => 1,
120                             disable_select => 1, #UI wonky, pry works otherwise
121                           },
122         'domsvc'    => {
123                          label     => 'Domain',
124                          type      => 'select',
125                          select_table => 'svc_domain',
126                          select_key   => 'svcnum',
127                          select_label => 'domain',
128                          disable_inventory => 1,
129                        },
130         'locationnum' => {
131                            label => 'E911 location',
132                            disable_inventory => 1,
133                            disable_select    => 1,
134                          },
135     },
136   };
137 }
138
139 sub table { 'svc_phone'; }
140
141 sub table_dupcheck_fields { ( 'countrycode', 'phonenum' ); }
142
143 =item search_sql STRING
144
145 Class method which returns an SQL fragment to search for the given string.
146
147 =cut
148
149 sub search_sql {
150   my( $class, $string ) = @_;
151
152   if ( $conf->exists('svc_phone-allow_alpha_phonenum') ) {
153     $string =~ s/\W//g;
154   } else {
155     $string =~ s/\D//g;
156   }
157
158   my $conf = new FS::Conf;
159   my $ccode = (    $conf->exists('default_phone_countrycode')
160                 && $conf->config('default_phone_countrycode')
161               )
162                 ? $conf->config('default_phone_countrycode') 
163                 : '1';
164
165   $string =~ s/^$ccode//;
166
167   $class->search_sql_field('phonenum', $string );
168 }
169
170 =item label
171
172 Returns the phone number.
173
174 =cut
175
176 sub label {
177   my $self = shift;
178   my $phonenum = $self->phonenum; #XXX format it better
179   my $label = $phonenum;
180   $label .= '@'.$self->domain if $self->domsvc;
181   $label .= ' ('.$self->phone_name.')' if $self->phone_name;
182   $label;
183 }
184
185 =item insert
186
187 Adds this phone number to the database.  If there is an error, returns the
188 error, otherwise returns false.
189
190 =cut
191
192 sub insert {
193   my $self = shift;
194   my %options = @_;
195
196   if ( $DEBUG ) {
197     warn "[$me] insert called on $self: ". Dumper($self).
198          "\nwith options: ". Dumper(%options);
199   }
200
201   local $SIG{HUP} = 'IGNORE';
202   local $SIG{INT} = 'IGNORE';
203   local $SIG{QUIT} = 'IGNORE';
204   local $SIG{TERM} = 'IGNORE';
205   local $SIG{TSTP} = 'IGNORE';
206   local $SIG{PIPE} = 'IGNORE';
207
208   my $oldAutoCommit = $FS::UID::AutoCommit;
209   local $FS::UID::AutoCommit = 0;
210   my $dbh = dbh;
211
212   #false laziness w/cust_pkg.pm... move this to location_Mixin?  that would
213   #make it more of a base class than a mixin... :)
214   if ( $options{'cust_location'}
215          && ( ! $self->locationnum || $self->locationnum == -1 ) ) {
216     my $error = $options{'cust_location'}->insert;
217     if ( $error ) {
218       $dbh->rollback if $oldAutoCommit;
219       return "inserting cust_location (transaction rolled back): $error";
220     }
221     $self->locationnum( $options{'cust_location'}->locationnum );
222   }
223   #what about on-the-fly edits?  if the ui supports it?
224
225   my $error = $self->SUPER::insert(%options);
226   if ( $error ) {
227     $dbh->rollback if $oldAutoCommit;
228     return $error;
229   }
230
231   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
232   '';
233
234 }
235
236 =item delete
237
238 Delete this record from the database.
239
240 =cut
241
242 sub delete {
243   my $self = shift;
244
245   local $SIG{HUP} = 'IGNORE';
246   local $SIG{INT} = 'IGNORE';
247   local $SIG{QUIT} = 'IGNORE';
248   local $SIG{TERM} = 'IGNORE';
249   local $SIG{TSTP} = 'IGNORE';
250   local $SIG{PIPE} = 'IGNORE';
251
252   my $oldAutoCommit = $FS::UID::AutoCommit;
253   local $FS::UID::AutoCommit = 0;
254   my $dbh = dbh;
255
256   foreach my $phone_device ( $self->phone_device ) {
257     my $error = $phone_device->delete;
258     if ( $error ) {
259       $dbh->rollback if $oldAutoCommit;
260       return $error;
261     }
262   }
263
264   my $error = $self->SUPER::delete;
265   if ( $error ) {
266     $dbh->rollback if $oldAutoCommit;
267     return $error;
268   }
269
270   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
271   '';
272
273 }
274
275 # the delete method can be inherited from FS::Record
276
277 =item replace OLD_RECORD
278
279 Replaces the OLD_RECORD with this one in the database.  If there is an error,
280 returns the error, otherwise returns false.
281
282 =cut
283
284 sub replace {
285   my $new = shift;
286
287   my $old = ( blessed($_[0]) && $_[0]->isa('FS::Record') )
288               ? shift
289               : $new->replace_old;
290
291   my %options = @_;
292
293   if ( $DEBUG ) {
294     warn "[$me] replacing $old with $new\n".
295          "\nwith options: ". Dumper(%options);
296   }
297
298   local $SIG{HUP} = 'IGNORE';
299   local $SIG{INT} = 'IGNORE';
300   local $SIG{QUIT} = 'IGNORE';
301   local $SIG{TERM} = 'IGNORE';
302   local $SIG{TSTP} = 'IGNORE';
303   local $SIG{PIPE} = 'IGNORE';
304
305   my $oldAutoCommit = $FS::UID::AutoCommit;
306   local $FS::UID::AutoCommit = 0;
307   my $dbh = dbh;
308
309   #false laziness w/cust_pkg.pm... move this to location_Mixin?  that would
310   #make it more of a base class than a mixin... :)
311   if ( $options{'cust_location'}
312          && ( ! $new->locationnum || $new->locationnum == -1 ) ) {
313     my $error = $options{'cust_location'}->insert;
314     if ( $error ) {
315       $dbh->rollback if $oldAutoCommit;
316       return "inserting cust_location (transaction rolled back): $error";
317     }
318     $new->locationnum( $options{'cust_location'}->locationnum );
319   }
320   #what about on-the-fly edits?  if the ui supports it?
321
322   my $error = $new->SUPER::replace($old, %options);
323   if ( $error ) {
324     $dbh->rollback if $oldAutoCommit;
325     return $error if $error;
326   }
327
328   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
329   ''; #no error
330 }
331
332 =item suspend
333
334 Called by the suspend method of FS::cust_pkg (see L<FS::cust_pkg>).
335
336 =item unsuspend
337
338 Called by the unsuspend method of FS::cust_pkg (see L<FS::cust_pkg>).
339
340 =item cancel
341
342 Called by the cancel method of FS::cust_pkg (see L<FS::cust_pkg>).
343
344 =item check
345
346 Checks all fields to make sure this is a valid phone number.  If there is
347 an error, returns the error, otherwise returns false.  Called by the insert
348 and replace methods.
349
350 =cut
351
352 # the check method should currently be supplied - FS::Record contains some
353 # data checking routines
354
355 sub check {
356   my $self = shift;
357
358   my $conf = new FS::Conf;
359
360   my $phonenum = $self->phonenum;
361   my $phonenum_check_method;
362   if ( $conf->exists('svc_phone-allow_alpha_phonenum') ) {
363     $phonenum =~ s/\W//g;
364     $phonenum_check_method = 'ut_alpha';
365   } else {
366     $phonenum =~ s/\D//g;
367     $phonenum_check_method = 'ut_number';
368   }
369   $self->phonenum($phonenum);
370
371   $self->locationnum('') if !$self->locationnum || $self->locationnum == -1;
372
373   my $error = 
374     $self->ut_numbern('svcnum')
375     || $self->ut_numbern('countrycode')
376     || $self->$phonenum_check_method('phonenum')
377     || $self->ut_anything('sip_password')
378     || $self->ut_numbern('pin')
379     || $self->ut_textn('phone_name')
380     || $self->ut_foreign_keyn('pbxsvc', 'svc_pbx',    'svcnum' )
381     || $self->ut_foreign_keyn('domsvc', 'svc_domain', 'svcnum' )
382     || $self->ut_foreign_keyn('locationnum', 'cust_location', 'locationnum')
383   ;
384   return $error if $error;
385
386   $self->countrycode(1) unless $self->countrycode;
387
388   unless ( length($self->sip_password) ) {
389
390     $self->sip_password(
391       join('', map $pw_set[ int(rand $#pw_set) ], (0..16) )
392     );
393
394   }
395
396   $self->SUPER::check;
397 }
398
399 =item _check duplicate
400
401 Internal method to check for duplicate phone numers.
402
403 =cut
404
405 #false laziness w/svc_acct.pm's _check_duplicate.
406 sub _check_duplicate {
407   my $self = shift;
408
409   my $global_unique = $conf->config('global_unique-phonenum') || 'none';
410   return '' if $global_unique eq 'disabled';
411
412   $self->lock_table;
413
414   my @dup_ccphonenum =
415     grep { !$self->svcnum || $_->svcnum != $self->svcnum }
416     qsearch( 'svc_phone', {
417       'countrycode' => $self->countrycode,
418       'phonenum'    => $self->phonenum,
419     });
420
421   return gettext('phonenum_in_use')
422     if $global_unique eq 'countrycode+phonenum' && @dup_ccphonenum;
423
424   my $part_svc = qsearchs('part_svc', { 'svcpart' => $self->svcpart } );
425   unless ( $part_svc ) {
426     return 'unknown svcpart '. $self->svcpart;
427   }
428
429   if ( @dup_ccphonenum ) {
430
431     my $exports = FS::part_export::export_info('svc_phone');
432     my %conflict_ccphonenum_svcpart = ( $self->svcpart => 'SELF', );
433
434     foreach my $part_export ( $part_svc->part_export ) {
435
436       #this will catch to the same exact export
437       my @svcparts = map { $_->svcpart } $part_export->export_svc;
438
439       $conflict_ccphonenum_svcpart{$_} = $part_export->exportnum
440         foreach @svcparts;
441
442     }
443
444     foreach my $dup_ccphonenum ( @dup_ccphonenum ) {
445       my $dup_svcpart = $dup_ccphonenum->cust_svc->svcpart;
446       if ( exists($conflict_ccphonenum_svcpart{$dup_svcpart}) ) {
447         return "duplicate phone number ".
448                $self->countrycode. ' '. $self->phonenum.
449                ": conflicts with svcnum ". $dup_ccphonenum->svcnum.
450                " via exportnum ". $conflict_ccphonenum_svcpart{$dup_svcpart};
451       }
452     }
453
454   }
455
456   return '';
457
458 }
459
460 =item check_pin
461
462 Checks the supplied PIN against the PIN in the database.  Returns true for a
463 sucessful authentication, false if no match.
464
465 =cut
466
467 sub check_pin {
468   my($self, $check_pin) = @_;
469   length($self->pin) && $check_pin eq $self->pin;
470 }
471
472 =item radius_reply
473
474 =cut
475
476 sub radius_reply {
477   my $self = shift;
478   #XXX Session-Timeout!  holy shit, need rlm_perl to ask for this in realtime
479   ();
480 }
481
482 =item radius_check
483
484 =cut
485
486 sub radius_check {
487   my $self = shift;
488   my %check = ();
489
490   my $conf = new FS::Conf;
491
492   $check{'User-Password'} = $conf->config('svc_phone-radius-default_password');
493
494   %check;
495 }
496
497 sub radius_groups {
498   ();
499 }
500
501 =item phone_device
502
503 Returns any FS::phone_device records associated with this service.
504
505 =cut
506
507 sub phone_device {
508   my $self = shift;
509   qsearch('phone_device', { 'svcnum' => $self->svcnum } );
510 }
511
512 #override location_Mixin version cause we want to try the cust_pkg location
513 #in between us and cust_main
514 # XXX what to do in the unlinked case???  return a pseudo-object that returns
515 # empty fields?
516 sub cust_location_or_main {
517   my $self = shift;
518   return $self->cust_location if $self->locationnum;
519   my $cust_pkg = $self->cust_svc->cust_pkg;
520   $cust_pkg ? $cust_pkg->cust_location_or_main : '';
521 }
522
523 =back
524
525 =head1 BUGS
526
527 =head1 SEE ALSO
528
529 L<FS::svc_Common>, L<FS::Record>, L<FS::cust_svc>, L<FS::part_svc>,
530 L<FS::cust_pkg>, schema.html from the base documentation.
531
532 =cut
533
534 1;
535