svc_cable service have a single serial / MAC / model, not one-to-many devices like...
[freeside.git] / FS / FS / svc_broadband.pm
1 package FS::svc_broadband;
2 use base qw(
3   FS::svc_Radius_Mixin
4   FS::svc_Tower_Mixin
5   FS::svc_IP_Mixin 
6   FS::svc_Common
7   );
8
9 use strict;
10 use vars qw($conf);
11
12 { no warnings 'redefine'; use NetAddr::IP; }
13 use FS::Record qw( qsearchs qsearch dbh );
14 use FS::cust_svc;
15 use FS::addr_block;
16 use FS::part_svc_router;
17 use FS::tower_sector;
18
19 $FS::UID::callback{'FS::svc_broadband'} = sub { 
20   $conf = new FS::Conf;
21 };
22
23 =head1 NAME
24
25 FS::svc_broadband - Object methods for svc_broadband records
26
27 =head1 SYNOPSIS
28
29   use FS::svc_broadband;
30
31   $record = new FS::svc_broadband \%hash;
32   $record = new FS::svc_broadband { '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_broadband object represents a 'broadband' Internet connection, such
51 as a DSL, cable modem, or fixed wireless link.  These services are assumed to
52 have the following properties:
53
54 FS::svc_broadband inherits from FS::svc_Common.  The following fields are
55 currently supported:
56
57 =over 4
58
59 =item svcnum - primary key
60
61 =item blocknum - see FS::addr_block
62
63 =item
64 speed_up - maximum upload speed, in bits per second.  If set to zero, upload
65 speed will be unlimited.  Exports that do traffic shaping should handle this
66 correctly, and not blindly set the upload speed to zero and kill the customer's
67 connection.
68
69 =item
70 speed_down - maximum download speed, as above
71
72 =item ip_addr - the customer's IP address.  If the customer needs more than one
73 IP address, set this to the address of the customer's router.  As a result, the
74 customer's router will have the same address for both its internal and external
75 interfaces thus saving address space.  This has been found to work on most NAT
76 routers available.
77
78 =item plan_id
79
80 =back
81
82 =head1 METHODS
83
84 =over 4
85
86 =item new HASHREF
87
88 Creates a new svc_broadband.  To add the record to the database, see
89 "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 sub table_info {
97   {
98     'name' => 'Wireless broadband',
99     'name_plural' => 'Wireless broadband services',
100     'longname_plural' => 'Fixed wireless broadband services',
101     'display_weight' => 50,
102     'cancel_weight'  => 70,
103     'ip_field' => 'ip_addr',
104     'fields' => {
105       'svcnum'      => 'Service',
106       'description' => 'Descriptive label',
107       'speed_down'  => 'Download speed (Kbps)',
108       'speed_up'    => 'Upload speed (Kbps)',
109       'ip_addr'     => 'IP address',
110       'blocknum'    => 
111       { 'label' => 'Address block',
112                          'type'  => 'select',
113                          'select_table' => 'addr_block',
114                           'select_key'   => 'blocknum',
115                          'select_label' => 'cidr',
116                          'disable_inventory' => 1,
117                        },
118      'plan_id' => 'Service Plan Id',
119      'performance_profile' => 'Peformance Profile',
120      'authkey'      => 'Authentication key',
121      'mac_addr'     => 'MAC address',
122      'latitude'     => 'Latitude',
123      'longitude'    => 'Longitude',
124      'altitude'     => 'Altitude',
125      'vlan_profile' => 'VLAN profile',
126      'sectornum'    => 'Tower/sector',
127      'routernum'    => 'Router/block',
128      'usergroup'    => { 
129                          label => 'RADIUS groups',
130                          type  => 'select-radius_group.html',
131                          #select_table => 'radius_group',
132                          #select_key   => 'groupnum',
133                          #select_label => 'groupname',
134                          disable_inventory => 1,
135                          multiple => 1,
136                        },
137       'radio_serialnum' => 'Radio Serial Number',
138       'radio_location'  => 'Radio Location',
139       'poe_location'    => 'POE Location',
140       'rssi'            => 'RSSI',
141       'suid'            => 'SUID',
142       'shared_svcnum'   => { label             => 'Shared Service',
143                              type              => 'search-svc_broadband',
144                              disable_inventory => 1,
145                            },
146     },
147   };
148 }
149
150 sub table { 'svc_broadband'; }
151
152 sub table_dupcheck_fields { ( 'ip_addr', 'mac_addr' ); }
153
154 =item search HASHREF
155
156 Class method which returns a qsearch hash expression to search for parameters
157 specified in HASHREF.
158
159 Parameters:
160
161 =over 4
162
163 =item unlinked - set to search for all unlinked services.  Overrides all other options.
164
165 =item agentnum
166
167 =item custnum
168
169 =item svcpart
170
171 =item ip_addr
172
173 =item pkgpart - arrayref
174
175 =item routernum - arrayref
176
177 =item sectornum - arrayref
178
179 =item towernum - arrayref
180
181 =item order_by
182
183 =back
184
185 =cut
186
187 sub _search_svc {
188   my( $class, $params, $from, $where ) = @_;
189
190   #routernum, can be arrayref
191   for my $routernum ( $params->{'routernum'} ) {
192     # this no longer uses addr_block
193     if ( ref $routernum and grep { $_ } @$routernum ) {
194       my $in = join(',', map { /^(\d+)$/ ? $1 : () } @$routernum );
195       my @orwhere = ();
196       push @orwhere, "svc_broadband.routernum IN ($in)" if $in;
197       push @orwhere, "svc_broadband.routernum IS NULL" 
198         if grep /^none$/, @$routernum;
199       push @$where, '( '.join(' OR ', @orwhere).' )';
200     }
201     elsif ( $routernum =~ /^(\d+)$/ ) {
202       push @$where, "svc_broadband.routernum = $1";
203     }
204     elsif ( $routernum eq 'none' ) {
205       push @$where, "svc_broadband.routernum IS NULL";
206     }
207   }
208
209   #this should probably move to svc_Tower_Mixin, or maybe we never should have
210   # done svc_acct # towers (or, as mark thought, never should have done
211   # svc_broadband)
212
213   #sector and tower, as above
214   my @where_sector = $class->tower_sector_sql($params);
215   if ( @where_sector ) {
216     push @$where, @where_sector;
217     push @$from, 'LEFT JOIN tower_sector USING ( sectornum )';
218   }
219  
220   #ip_addr
221   if ( $params->{'ip_addr'} =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/ ) {
222     push @$where, "ip_addr = '$1'";
223   }
224
225 }
226
227 =item search_sql STRING
228
229 Class method which returns an SQL fragment to search for the given string.
230
231 =cut
232
233 sub search_sql {
234   my( $class, $string ) = @_;
235   if ( $string =~ /^(\d{1,3}\.){3}\d{1,3}$/ ) {
236     $class->search_sql_field('ip_addr', $string );
237   } elsif ( $string =~ /^([A-F0-9]{12})$/i ) {
238     $class->search_sql_field('mac_addr', uc($string));
239   } elsif ( $string =~ /^(([A-F0-9]{2}:){5}([A-F0-9]{2}))$/i ) {
240     $string =~ s/://g;
241     $class->search_sql_field('mac_addr', uc($string) );
242   } elsif ( $string =~ /^(\d+)$/ ) {
243     my $table = $class->table;
244     "$table.svcnum = $1";
245   } else {
246     '1 = 0'; #false
247   }
248 }
249
250 =item smart_search STRING
251
252 =cut
253
254 sub smart_search {
255   my( $class, $string ) = @_;
256   qsearch({
257     'table'     => $class->table, #'svc_broadband',
258     'hashref'   => {},
259     'extra_sql' => 'WHERE '. $class->search_sql($string),
260   });
261 }
262
263 =item label
264
265 Returns the IP address.
266
267 =cut
268
269 sub label {
270   my $self = shift;
271   my $label = 'IP:'. ($self->ip_addr || 'Unknown');
272   $label .= ', MAC:'. $self->mac_addr
273     if $self->mac_addr;
274   $label .= ' ('. $self->description. ')'
275     if $self->description;
276   return $label;
277 }
278
279 =item insert [ , OPTION => VALUE ... ]
280
281 Adds this record to the database.  If there is an error, returns the error,
282 otherwise returns false.
283
284 The additional fields pkgnum and svcpart (see FS::cust_svc) should be 
285 defined.  An FS::cust_svc record will be created and inserted.
286
287 Currently available options are: I<depend_jobnum>
288
289 If I<depend_jobnum> is set (to a scalar jobnum or an array reference of
290 jobnums), all provisioning jobs will have a dependancy on the supplied
291 jobnum(s) (they will not run until the specific job(s) complete(s)).
292
293 =cut
294
295 # Standard FS::svc_Common::insert
296
297 =item delete
298
299 Delete this record from the database.
300
301 =cut
302
303 # Standard FS::svc_Common::delete
304
305 =item replace OLD_RECORD
306
307 Replaces the OLD_RECORD with this one in the database.  If there is an error,
308 returns the error, otherwise returns false.
309
310 # Standard FS::svc_Common::replace
311
312 =item suspend
313
314 Called by the suspend method of FS::cust_pkg (see FS::cust_pkg).
315
316 =item unsuspend
317
318 Called by the unsuspend method of FS::cust_pkg (see FS::cust_pkg).
319
320 =item cancel
321
322 Called by the cancel method of FS::cust_pkg (see FS::cust_pkg).
323
324 =item check
325
326 Checks all fields to make sure this is a valid broadband service.  If there is
327 an error, returns the error, otherwise returns false.  Called by the insert
328 and replace methods.
329
330 =cut
331
332 sub check {
333   my $self = shift;
334   my $x = $self->setfixed;
335
336   return $x unless ref($x);
337
338   # remove delimiters
339   my $mac_addr = uc($self->get('mac_addr'));
340   $mac_addr =~ s/[\W_]//g;
341   $self->set('mac_addr', $mac_addr);
342
343   my $error =
344     $self->ut_numbern('svcnum')
345     || $self->ut_numbern('blocknum')
346     || $self->ut_foreign_keyn('routernum', 'router', 'routernum')
347     || $self->ut_foreign_keyn('sectornum', 'tower_sector', 'sectornum')
348     || $self->ut_textn('description')
349     || $self->ut_numbern('speed_up')
350     || $self->ut_numbern('speed_down')
351     || $self->ut_ipn('ip_addr')
352     || $self->ut_hexn('mac_addr')
353     || $self->ut_hexn('auth_key')
354     || $self->ut_coordn('latitude')
355     || $self->ut_coordn('longitude')
356     || $self->ut_sfloatn('altitude')
357     || $self->ut_textn('vlan_profile')
358     || $self->ut_textn('plan_id')
359     || $self->ut_alphan('radio_serialnum')
360     || $self->ut_textn('radio_location')
361     || $self->ut_textn('poe_location')
362     || $self->ut_snumbern('rssi')
363     || $self->ut_numbern('suid')
364     || $self->ut_foreign_keyn('shared_svcnum', 'svc_broadband', 'svcnum')
365   ;
366   return $error if $error;
367
368   if(($self->speed_up || 0) < 0) { return 'speed_up must be positive'; }
369   if(($self->speed_down || 0) < 0) { return 'speed_down must be positive'; }
370
371   my $cust_svc = $self->svcnum
372                  ? qsearchs('cust_svc', { 'svcnum' => $self->svcnum } )
373                  : '';
374   my $cust_pkg;
375   my $svcpart;
376   if ($cust_svc) {
377     $cust_pkg = $cust_svc->cust_pkg;
378     $svcpart = $cust_svc->svcpart;
379   }else{
380     $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $self->pkgnum } );
381     return "Invalid pkgnum" unless $cust_pkg;
382     $svcpart = $self->svcpart;
383   }
384   my $agentnum = $cust_pkg->cust_main->agentnum if $cust_pkg;
385
386   # assign IP address / router / block
387   $error = $self->svc_ip_check;
388   return $error if $error;
389   if ( !$self->ip_addr 
390        and !$conf->exists('svc_broadband-allow_null_ip_addr') ) {
391     return 'IP address is required';
392   }
393
394   if ( $cust_pkg && ! $self->latitude && ! $self->longitude ) {
395     my $l = $cust_pkg->cust_location_or_main;
396     if ( $l->ship_latitude && $l->ship_longitude ) {
397       $self->latitude(  $l->ship_latitude  );
398       $self->longitude( $l->ship_longitude );
399     } elsif ( $l->latitude && $l->longitude ) {
400       $self->latitude(  $l->latitude  );
401       $self->longitude( $l->longitude );
402     }
403   }
404
405   $self->SUPER::check;
406 }
407
408 sub _check_duplicate {
409   my $self = shift;
410   # Not a reliable check because the table isn't locked, but 
411   # that's why we have a unique index.  This is just to give a
412   # friendlier error message.
413   my @dup;
414   @dup = $self->find_duplicates('global', 'mac_addr');
415   if ( @dup ) {
416     return "MAC address in use (svcnum ".$dup[0]->svcnum.")";
417   }
418
419   '';
420 }
421
422 =item mac_addr_formatted CASE DELIMITER
423
424 Format the MAC address (for use by exports).  If CASE starts with "l"
425 (for "lowercase"), it's returned in lowercase.  DELIMITER is inserted
426 between octets.
427
428 =cut
429
430 sub mac_addr_formatted {
431   my $self = shift;
432   my ($case, $delim) = @_;
433   my $addr = $self->mac_addr;
434   $addr = lc($addr) if $case =~ /^l/i;
435   join( $delim || '', $addr =~ /../g );
436 }
437
438 #class method
439 sub _upgrade_data {
440   my $class = shift;
441
442   local($FS::svc_Common::noexport_hack) = 1;
443
444   # fix wrong-case MAC addresses
445   my $dbh = dbh;
446   $dbh->do('UPDATE svc_broadband SET mac_addr = UPPER(mac_addr);')
447     or die $dbh->errstr;
448
449   # set routernum to addr_block.routernum
450   foreach my $self (qsearch('svc_broadband', {
451       blocknum => {op => '!=', value => ''},
452       routernum => ''
453     })) {
454     my $addr_block = $self->addr_block;
455     if ( !$addr_block ) {
456       # super paranoid mode
457       warn "WARNING: svcnum ".$self->svcnum." is assigned to addr_block ".$self->blocknum.", which does not exist; skipped.\n";
458       next;
459     }
460     my $ip_addr = $self->ip_addr;
461     my $routernum = $addr_block->routernum;
462     if ( $routernum ) {
463       $self->set(routernum => $routernum);
464       my $error = $self->check;
465       # sanity check: don't allow this to change IP address or block
466       # (other than setting blocknum to null for a non-auto-assigned router)
467       if ( $self->ip_addr ne $ip_addr 
468         or ($self->blocknum and $self->blocknum != $addr_block->blocknum)) {
469         warn "WARNING: Upgrading service ".$self->svcnum." would change its block/address; skipped.\n";
470         next;
471       }
472
473       $error ||= $self->replace;
474       warn "WARNING: error assigning routernum $routernum to service ".$self->svcnum.
475           ":\n$error; skipped\n"
476         if $error;
477     }
478     else {
479       warn "svcnum ".$self->svcnum.
480         ": no routernum in address block ".$addr_block->cidr.", skipped\n";
481     }
482   }
483
484   # assign blocknums to services that should have them
485   my @all_blocks = qsearch('addr_block', { });
486   SVC: foreach my $self ( 
487     qsearch({
488         'select' => 'svc_broadband.*',
489         'table' => 'svc_broadband',
490         'addl_from' => 'JOIN router USING (routernum)',
491         'hashref' => {},
492         'extra_sql' => 'WHERE svc_broadband.blocknum IS NULL '.
493                        'AND router.manual_addr IS NULL',
494     }) 
495   ) {
496    
497     next SVC if $self->ip_addr eq '';
498     my $NetAddr = $self->NetAddr;
499     # inefficient, but should only need to run once
500     foreach my $block (@all_blocks) {
501       if ($block->NetAddr->contains($NetAddr)) {
502         $self->set(blocknum => $block->blocknum);
503         my $error = $self->replace;
504         warn "WARNING: error assigning blocknum ".$block->blocknum.
505         " to service ".$self->svcnum."\n$error; skipped\n"
506           if $error;
507         next SVC;
508       }
509     }
510     warn "WARNING: no block found containing ".$NetAddr->addr." for service ".
511       $self->svcnum;
512     #next SVC;
513   }
514
515   '';
516 }
517
518 =back
519
520 =head1 BUGS
521
522 The business with sb_field has been 'fixed', in a manner of speaking.
523
524 allowed_routers isn't agent virtualized because part_svc isn't agent
525 virtualized
526
527 Having both routernum and blocknum as foreign keys is somewhat dubious.
528
529 =head1 SEE ALSO
530
531 FS::svc_Common, FS::Record, FS::addr_block,
532 FS::part_svc, schema.html from the base documentation.
533
534 =cut
535
536 1;
537