linking DIDs and users to PBXes, RT#7051
[freeside.git] / FS / FS / svc_phone.pm
1 package FS::svc_phone;
2
3 use strict;
4 use vars qw( @ISA @pw_set $conf );
5 use FS::Conf;
6 use FS::Record qw( qsearch qsearchs dbh );
7 use FS::Msgcat qw(gettext);
8 use FS::svc_Common;
9 use FS::part_svc;
10 use FS::phone_device;
11 use FS::svc_pbx;
12
13 @ISA = qw( FS::svc_Common );
14
15 #avoid l 1 and o O 0
16 @pw_set = ( 'a'..'k', 'm','n', 'p-z', 'A'..'N', 'P'..'Z' , '2'..'9' );
17
18 #ask FS::UID to run this stuff for us later
19 $FS::UID::callback{'FS::svc_acct'} = sub { 
20   $conf = new FS::Conf;
21 };
22
23 =head1 NAME
24
25 FS::svc_phone - Object methods for svc_phone records
26
27 =head1 SYNOPSIS
28
29   use FS::svc_phone;
30
31   $record = new FS::svc_phone \%hash;
32   $record = new FS::svc_phone { 'column' => 'value' };
33
34   $error = $record->insert;
35
36   $error = $new_record->replace($old_record);
37
38   $error = $record->delete;
39
40   $error = $record->check;
41
42   $error = $record->suspend;
43
44   $error = $record->unsuspend;
45
46   $error = $record->cancel;
47
48 =head1 DESCRIPTION
49
50 An FS::svc_phone object represents a phone number.  FS::svc_phone inherits
51 from FS::Record.  The following fields are currently supported:
52
53 =over 4
54
55 =item svcnum
56
57 primary key
58
59 =item countrycode
60
61 =item phonenum
62
63 =item sip_password
64
65 =item pin
66
67 Voicemail PIN
68
69 =item phone_name
70
71 =item pbxsvc
72
73 Optional svcnum from svc_pbx
74
75 =back
76
77 =head1 METHODS
78
79 =over 4
80
81 =item new HASHREF
82
83 Creates a new phone number.  To add the number to the database, see L<"insert">.
84
85 Note that this stores the hash reference, not a distinct copy of the hash it
86 points to.  You can ask the object for a copy with the I<hash> method.
87
88 =cut
89
90 # the new method can be inherited from FS::Record, if a table method is defined
91 #
92 sub table_info {
93   {
94     'name' => 'Phone number',
95     'sorts' => 'phonenum',
96     'display_weight' => 60,
97     'cancel_weight'  => 80,
98     'fields' => {
99         'countrycode'  => { label => 'Country code',
100                             type  => 'text',
101                             disable_inventory => 1,
102                             disable_select => 1,
103                           },
104         'phonenum'     => 'Phone number',
105         'pin'          => { label => 'Personal Identification Number',
106                             type  => 'text',
107                             disable_inventory => 1,
108                             disable_select => 1,
109                           },
110         'sip_password' => 'SIP password',
111         'phone_name'   => 'Name',
112         'pbxsvc'       => { label => 'PBX',
113                             type  => 'select-svc_pbx.html',
114                             disable_inventory => 1,
115                             disable_select => 1, #UI wonky, pry works otherwise
116                           },
117     },
118   };
119 }
120
121 sub table { 'svc_phone'; }
122
123 sub table_dupcheck_fields { ( 'countrycode', 'phonenum' ); }
124
125 =item search_sql STRING
126
127 Class method which returns an SQL fragment to search for the given string.
128
129 =cut
130
131 sub search_sql {
132   my( $class, $string ) = @_;
133
134   if ( $conf->exists('svc_phone-allow_alpha_phonenum') ) {
135     $string =~ s/\W//g;
136   } else {
137     $string =~ s/\D//g;
138   }
139
140   my $conf = new FS::Conf;
141   my $ccode = (    $conf->exists('default_phone_countrycode')
142                 && $conf->config('default_phone_countrycode')
143               )
144                 ? $conf->config('default_phone_countrycode') 
145                 : '1';
146
147   $string =~ s/^$ccode//;
148
149   $class->search_sql_field('phonenum', $string );
150 }
151
152 =item label
153
154 Returns the phone number.
155
156 =cut
157
158 sub label {
159   my $self = shift;
160   my $phonenum = $self->phonenum; #XXX format it better
161   my $label = $phonenum;
162   $label .= ' ('.$self->phone_name.')' if $self->phone_name;
163   $label;
164 }
165
166 =item insert
167
168 Adds this record to the database.  If there is an error, returns the error,
169 otherwise returns false.
170
171 =cut
172
173 # the insert method can be inherited from FS::Record
174
175 =item delete
176
177 Delete this record from the database.
178
179 =cut
180
181 sub delete {
182   my $self = shift;
183
184   local $SIG{HUP} = 'IGNORE';
185   local $SIG{INT} = 'IGNORE';
186   local $SIG{QUIT} = 'IGNORE';
187   local $SIG{TERM} = 'IGNORE';
188   local $SIG{TSTP} = 'IGNORE';
189   local $SIG{PIPE} = 'IGNORE';
190
191   my $oldAutoCommit = $FS::UID::AutoCommit;
192   local $FS::UID::AutoCommit = 0;
193   my $dbh = dbh;
194
195   foreach my $phone_device ( $self->phone_device ) {
196     my $error = $phone_device->delete;
197     if ( $error ) {
198       $dbh->rollback if $oldAutoCommit;
199       return $error;
200     }
201   }
202
203   my $error = $self->SUPER::delete;
204   if ( $error ) {
205     $dbh->rollback if $oldAutoCommit;
206     return $error;
207   }
208
209   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
210   '';
211
212 }
213
214 # the delete method can be inherited from FS::Record
215
216 =item replace OLD_RECORD
217
218 Replaces the OLD_RECORD with this one in the database.  If there is an error,
219 returns the error, otherwise returns false.
220
221 =cut
222
223 # the replace method can be inherited from FS::Record
224
225 =item suspend
226
227 Called by the suspend method of FS::cust_pkg (see L<FS::cust_pkg>).
228
229 =item unsuspend
230
231 Called by the unsuspend method of FS::cust_pkg (see L<FS::cust_pkg>).
232
233 =item cancel
234
235 Called by the cancel method of FS::cust_pkg (see L<FS::cust_pkg>).
236
237 =item check
238
239 Checks all fields to make sure this is a valid phone number.  If there is
240 an error, returns the error, otherwise returns false.  Called by the insert
241 and replace methods.
242
243 =cut
244
245 # the check method should currently be supplied - FS::Record contains some
246 # data checking routines
247
248 sub check {
249   my $self = shift;
250
251   my $conf = new FS::Conf;
252
253   my $phonenum = $self->phonenum;
254   my $phonenum_check_method;
255   if ( $conf->exists('svc_phone-allow_alpha_phonenum') ) {
256     $phonenum =~ s/\W//g;
257     $phonenum_check_method = 'ut_alpha';
258   } else {
259     $phonenum =~ s/\D//g;
260     $phonenum_check_method = 'ut_number';
261   }
262   $self->phonenum($phonenum);
263
264   my $error = 
265     $self->ut_numbern('svcnum')
266     || $self->ut_numbern('countrycode')
267     || $self->$phonenum_check_method('phonenum')
268     || $self->ut_anything('sip_password')
269     || $self->ut_numbern('pin')
270     || $self->ut_textn('phone_name')
271     || $self->ut_foreign_keyn('pbxsvc', 'svc_pbx', 'svcnum' )
272   ;
273   return $error if $error;
274
275   $self->countrycode(1) unless $self->countrycode;
276
277   unless ( length($self->sip_password) ) {
278
279     $self->sip_password(
280       join('', map $pw_set[ int(rand $#pw_set) ], (0..16) )
281     );
282
283   }
284
285   $self->SUPER::check;
286 }
287
288 =item _check duplicate
289
290 Internal method to check for duplicate phone numers.
291
292 =cut
293
294 #false laziness w/svc_acct.pm's _check_duplicate.
295 sub _check_duplicate {
296   my $self = shift;
297
298   my $global_unique = $conf->config('global_unique-phonenum') || 'none';
299   return '' if $global_unique eq 'disabled';
300
301   $self->lock_table;
302
303   my @dup_ccphonenum =
304     grep { !$self->svcnum || $_->svcnum != $self->svcnum }
305     qsearch( 'svc_phone', {
306       'countrycode' => $self->countrycode,
307       'phonenum'    => $self->phonenum,
308     });
309
310   return gettext('phonenum_in_use')
311     if $global_unique eq 'countrycode+phonenum' && @dup_ccphonenum;
312
313   my $part_svc = qsearchs('part_svc', { 'svcpart' => $self->svcpart } );
314   unless ( $part_svc ) {
315     return 'unknown svcpart '. $self->svcpart;
316   }
317
318   if ( @dup_ccphonenum ) {
319
320     my $exports = FS::part_export::export_info('svc_phone');
321     my %conflict_ccphonenum_svcpart = ( $self->svcpart => 'SELF', );
322
323     foreach my $part_export ( $part_svc->part_export ) {
324
325       #this will catch to the same exact export
326       my @svcparts = map { $_->svcpart } $part_export->export_svc;
327
328       $conflict_ccphonenum_svcpart{$_} = $part_export->exportnum
329         foreach @svcparts;
330
331     }
332
333     foreach my $dup_ccphonenum ( @dup_ccphonenum ) {
334       my $dup_svcpart = $dup_ccphonenum->cust_svc->svcpart;
335       if ( exists($conflict_ccphonenum_svcpart{$dup_svcpart}) ) {
336         return "duplicate phone number ".
337                $self->countrycode. ' '. $self->phonenum.
338                ": conflicts with svcnum ". $dup_ccphonenum->svcnum.
339                " via exportnum ". $conflict_ccphonenum_svcpart{$dup_svcpart};
340       }
341     }
342
343   }
344
345   return '';
346
347 }
348
349 =item check_pin
350
351 Checks the supplied PIN against the PIN in the database.  Returns true for a
352 sucessful authentication, false if no match.
353
354 =cut
355
356 sub check_pin {
357   my($self, $check_pin) = @_;
358   length($self->pin) && $check_pin eq $self->pin;
359 }
360
361 =item radius_reply
362
363 =cut
364
365 sub radius_reply {
366   my $self = shift;
367   #XXX Session-Timeout!  holy shit, need rlm_perl to ask for this in realtime
368   ();
369 }
370
371 =item radius_check
372
373 =cut
374
375 sub radius_check {
376   my $self = shift;
377   my %check = ();
378
379   my $conf = new FS::Conf;
380
381   $check{'User-Password'} = $conf->config('svc_phone-radius-default_password');
382
383   %check;
384 }
385
386 sub radius_groups {
387   ();
388 }
389
390 =item phone_device
391
392 Returns any FS::phone_device records associated with this service.
393
394 =cut
395
396 sub phone_device {
397   my $self = shift;
398   qsearch('phone_device', { 'svcnum' => $self->svcnum } );
399 }
400
401 =back
402
403 =head1 BUGS
404
405 =head1 SEE ALSO
406
407 L<FS::svc_Common>, L<FS::Record>, L<FS::cust_svc>, L<FS::part_svc>,
408 L<FS::cust_pkg>, schema.html from the base documentation.
409
410 =cut
411
412 1;
413