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