add MAC address and description to svc_broadband label, RT#19115
[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 for this particular device',
107       'speed_down'  => 'Maximum download speed for this service in Kbps.  0 denotes unlimited.',
108       'speed_up'    => 'Maximum upload speed for this service in Kbps.  0 denotes unlimited.',
109       'ip_addr'     => 'IP address.  Leave blank for automatic assignment.',
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     },
138   };
139 }
140
141 sub table { 'svc_broadband'; }
142
143 sub table_dupcheck_fields { ( 'ip_addr', 'mac_addr' ); }
144
145 =item search HASHREF
146
147 Class method which returns a qsearch hash expression to search for parameters
148 specified in HASHREF.
149
150 Parameters:
151
152 =over 4
153
154 =item unlinked - set to search for all unlinked services.  Overrides all other options.
155
156 =item agentnum
157
158 =item custnum
159
160 =item svcpart
161
162 =item ip_addr
163
164 =item pkgpart - arrayref
165
166 =item routernum - arrayref
167
168 =item sectornum - arrayref
169
170 =item towernum - arrayref
171
172 =item order_by
173
174 =back
175
176 =cut
177
178 sub search {
179   my ($class, $params) = @_;
180   my @where = ();
181   my @from = (
182     'LEFT JOIN cust_svc  USING ( svcnum  )',
183     'LEFT JOIN part_svc  USING ( svcpart )',
184     'LEFT JOIN cust_pkg  USING ( pkgnum  )',
185     FS::UI::Web::join_cust_main('cust_pkg', 'cust_pkg'),
186   );
187
188   # based on FS::svc_acct::search, probably the most mature of the bunch
189   #unlinked
190   push @where, 'pkgnum IS NULL' if $params->{'unlinked'};
191   
192   #agentnum
193   if ( $params->{'agentnum'} =~ /^(\d+)$/ and $1 ) {
194     push @where, "cust_main.agentnum = $1";
195   }
196   push @where, $FS::CurrentUser::CurrentUser->agentnums_sql(
197     'null_right' => 'View/link unlinked services',
198     'table' => 'cust_main'
199   );
200
201   #custnum
202   if ( $params->{'custnum'} =~ /^(\d+)$/ and $1 ) {
203     push @where, "custnum = $1";
204   }
205
206   #pkgpart, now properly untainted, can be arrayref
207   for my $pkgpart ( $params->{'pkgpart'} ) {
208     if ( ref $pkgpart ) {
209       my $where = join(',', map { /^(\d+)$/ ? $1 : () } @$pkgpart );
210       push @where, "cust_pkg.pkgpart IN ($where)" if $where;
211     }
212     elsif ( $pkgpart =~ /^(\d+)$/ ) {
213       push @where, "cust_pkg.pkgpart = $1";
214     }
215   }
216
217   #routernum, can be arrayref
218   for my $routernum ( $params->{'routernum'} ) {
219     # this no longer uses addr_block
220     if ( ref $routernum and grep { $_ } @$routernum ) {
221       my $in = join(',', map { /^(\d+)$/ ? $1 : () } @$routernum );
222       my @orwhere;
223       push @orwhere, "svc_broadband.routernum IN ($in)" if $in;
224       push @orwhere, "svc_broadband.routernum IS NULL" 
225         if grep /^none$/, @$routernum;
226       push @where, '( '.join(' OR ', @orwhere).' )';
227     }
228     elsif ( $routernum =~ /^(\d+)$/ ) {
229       push @where, "svc_broadband.routernum = $1";
230     }
231     elsif ( $routernum eq 'none' ) {
232       push @where, "svc_broadband.routernum IS NULL";
233     }
234   }
235
236   #sector and tower, as above
237   my @where_sector = $class->tower_sector_sql($params);
238   if ( @where_sector ) {
239     push @where, @where_sector;
240     push @from, 'LEFT JOIN tower_sector USING ( sectornum )';
241   }
242  
243   #svcnum
244   if ( $params->{'svcnum'} =~ /^(\d+)$/ ) {
245     push @where, "svcnum = $1";
246   }
247
248   #svcpart
249   if ( $params->{'svcpart'} =~ /^(\d+)$/ ) {
250     push @where, "svcpart = $1";
251   }
252
253   #exportnum
254   if ( $params->{'exportnum'} =~ /^(\d+)$/ ) {
255     push @from, 'LEFT JOIN export_svc USING ( svcpart )';
256     push @where, "exportnum = $1";
257   }
258
259   #ip_addr
260   if ( $params->{'ip_addr'} =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/ ) {
261     push @where, "ip_addr = '$1'";
262   }
263
264   #custnum
265   if ( $params->{'custnum'} =~ /^(\d+)$/ and $1) {
266     push @where, "custnum = $1";
267   }
268   
269   my $addl_from = join(' ', @from);
270   my $extra_sql = '';
271   $extra_sql = 'WHERE '.join(' AND ', @where) if @where;
272   my $count_query = "SELECT COUNT(*) FROM svc_broadband $addl_from $extra_sql";
273   return( {
274       'table'   => 'svc_broadband',
275       'hashref' => {},
276       'select'  => join(', ',
277         'svc_broadband.*',
278         'part_svc.svc',
279         'cust_main.custnum',
280         FS::UI::Web::cust_sql_fields($params->{'cust_fields'}),
281       ),
282       'extra_sql' => $extra_sql,
283       'addl_from' => $addl_from,
284       'order_by'  => "ORDER BY ".($params->{'order_by'} || 'svcnum'),
285       'count_query' => $count_query,
286     } );
287 }
288
289 =item search_sql STRING
290
291 Class method which returns an SQL fragment to search for the given string.
292
293 =cut
294
295 sub search_sql {
296   my( $class, $string ) = @_;
297   if ( $string =~ /^(\d{1,3}\.){3}\d{1,3}$/ ) {
298     $class->search_sql_field('ip_addr', $string );
299   }elsif ( $string =~ /^([a-fA-F0-9]{12})$/ ) {
300     $class->search_sql_field('mac_addr', uc($string));
301   }elsif ( $string =~ /^(([a-fA-F0-9]{1,2}:){5}([a-fA-F0-9]{1,2}))$/ ) {
302     $class->search_sql_field('mac_addr', uc("$2$3$4$5$6$7") );
303   } else {
304     '1 = 0'; #false
305   }
306 }
307
308 =item label
309
310 Returns the IP address.
311
312 =cut
313
314 sub label {
315   my $self = shift;
316   my $label = 'IP:'. ($self->ip_addr || 'Unknown');
317   $label .= '", MAC:'. $self->mac_addr
318     if $self->mac_addr;
319   $label .= ' ('. $self->description. ')'
320     if $self->description;
321   return $label;
322 }
323
324 =item insert [ , OPTION => VALUE ... ]
325
326 Adds this record to the database.  If there is an error, returns the error,
327 otherwise returns false.
328
329 The additional fields pkgnum and svcpart (see FS::cust_svc) should be 
330 defined.  An FS::cust_svc record will be created and inserted.
331
332 Currently available options are: I<depend_jobnum>
333
334 If I<depend_jobnum> is set (to a scalar jobnum or an array reference of
335 jobnums), all provisioning jobs will have a dependancy on the supplied
336 jobnum(s) (they will not run until the specific job(s) complete(s)).
337
338 =cut
339
340 # Standard FS::svc_Common::insert
341
342 =item delete
343
344 Delete this record from the database.
345
346 =cut
347
348 # Standard FS::svc_Common::delete
349
350 =item replace OLD_RECORD
351
352 Replaces the OLD_RECORD with this one in the database.  If there is an error,
353 returns the error, otherwise returns false.
354
355 # Standard FS::svc_Common::replace
356
357 =item suspend
358
359 Called by the suspend method of FS::cust_pkg (see FS::cust_pkg).
360
361 =item unsuspend
362
363 Called by the unsuspend method of FS::cust_pkg (see FS::cust_pkg).
364
365 =item cancel
366
367 Called by the cancel method of FS::cust_pkg (see FS::cust_pkg).
368
369 =item check
370
371 Checks all fields to make sure this is a valid broadband service.  If there is
372 an error, returns the error, otherwise returns false.  Called by the insert
373 and replace methods.
374
375 =cut
376
377 sub check {
378   my $self = shift;
379   my $x = $self->setfixed;
380
381   return $x unless ref($x);
382
383   # remove delimiters
384   my $mac_addr = uc($self->get('mac_addr'));
385   $mac_addr =~ s/[-: ]//g;
386   $self->set('mac_addr', $mac_addr);
387
388   my $error =
389     $self->ut_numbern('svcnum')
390     || $self->ut_numbern('blocknum')
391     || $self->ut_foreign_keyn('routernum', 'router', 'routernum')
392     || $self->ut_foreign_keyn('sectornum', 'tower_sector', 'sectornum')
393     || $self->ut_textn('description')
394     || $self->ut_numbern('speed_up')
395     || $self->ut_numbern('speed_down')
396     || $self->ut_ipn('ip_addr')
397     || $self->ut_hexn('mac_addr')
398     || $self->ut_hexn('auth_key')
399     || $self->ut_coordn('latitude')
400     || $self->ut_coordn('longitude')
401     || $self->ut_sfloatn('altitude')
402     || $self->ut_textn('vlan_profile')
403     || $self->ut_textn('plan_id')
404   ;
405   return $error if $error;
406
407   if(($self->speed_up || 0) < 0) { return 'speed_up must be positive'; }
408   if(($self->speed_down || 0) < 0) { return 'speed_down must be positive'; }
409
410   my $cust_svc = $self->svcnum
411                  ? qsearchs('cust_svc', { 'svcnum' => $self->svcnum } )
412                  : '';
413   my $cust_pkg;
414   my $svcpart;
415   if ($cust_svc) {
416     $cust_pkg = $cust_svc->cust_pkg;
417     $svcpart = $cust_svc->svcpart;
418   }else{
419     $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $self->pkgnum } );
420     return "Invalid pkgnum" unless $cust_pkg;
421     $svcpart = $self->svcpart;
422   }
423   my $agentnum = $cust_pkg->cust_main->agentnum if $cust_pkg;
424
425   # assign IP address / router / block
426   $error = $self->svc_ip_check;
427   return $error if $error;
428   if ( !$self->ip_addr 
429        and !$conf->exists('svc_broadband-allow_null_ip_addr') ) {
430     return 'IP address is required';
431   }
432
433   if ( $cust_pkg && ! $self->latitude && ! $self->longitude ) {
434     my $l = $cust_pkg->cust_location_or_main;
435     if ( $l->ship_latitude && $l->ship_longitude ) {
436       $self->latitude(  $l->ship_latitude  );
437       $self->longitude( $l->ship_longitude );
438     } elsif ( $l->latitude && $l->longitude ) {
439       $self->latitude(  $l->latitude  );
440       $self->longitude( $l->longitude );
441     }
442   }
443
444   $self->SUPER::check;
445 }
446
447 sub _check_duplicate {
448   my $self = shift;
449   # Not a reliable check because the table isn't locked, but 
450   # that's why we have a unique index.  This is just to give a
451   # friendlier error message.
452   my @dup;
453   @dup = $self->find_duplicates('global', 'mac_addr');
454   if ( @dup ) {
455     return "MAC address in use (svcnum ".$dup[0]->svcnum.")";
456   }
457
458   '';
459 }
460
461 =item mac_addr_formatted CASE DELIMITER
462
463 Format the MAC address (for use by exports).  If CASE starts with "l"
464 (for "lowercase"), it's returned in lowercase.  DELIMITER is inserted
465 between octets.
466
467 =cut
468
469 sub mac_addr_formatted {
470   my $self = shift;
471   my ($case, $delim) = @_;
472   my $addr = $self->mac_addr;
473   $addr = lc($addr) if $case =~ /^l/i;
474   join( $delim || '', $addr =~ /../g );
475 }
476
477 #class method
478 sub _upgrade_data {
479   my $class = shift;
480
481   local($FS::svc_Common::noexport_hack) = 1;
482
483   # set routernum to addr_block.routernum
484   foreach my $self (qsearch('svc_broadband', {
485       blocknum => {op => '!=', value => ''},
486       routernum => ''
487     })) {
488     my $addr_block = $self->addr_block;
489     if ( !$addr_block ) {
490       # super paranoid mode
491       warn "WARNING: svcnum ".$self->svcnum." is assigned to addr_block ".$self->blocknum.", which does not exist; skipped.\n";
492       next;
493     }
494     my $ip_addr = $self->ip_addr;
495     my $routernum = $addr_block->routernum;
496     if ( $routernum ) {
497       $self->set(routernum => $routernum);
498       my $error = $self->check;
499       # sanity check: don't allow this to change IP address or block
500       # (other than setting blocknum to null for a non-auto-assigned router)
501       if ( $self->ip_addr ne $ip_addr 
502         or ($self->blocknum and $self->blocknum != $addr_block->blocknum)) {
503         warn "WARNING: Upgrading service ".$self->svcnum." would change its block/address; skipped.\n";
504         next;
505       }
506
507       $error ||= $self->replace;
508       warn "WARNING: error assigning routernum $routernum to service ".$self->svcnum.
509           ":\n$error; skipped\n"
510         if $error;
511     }
512     else {
513       warn "svcnum ".$self->svcnum.
514         ": no routernum in address block ".$addr_block->cidr.", skipped\n";
515     }
516   }
517
518   # assign blocknums to services that should have them
519   my @all_blocks = qsearch('addr_block', { });
520   SVC: foreach my $self ( 
521     qsearch({
522         'select' => 'svc_broadband.*',
523         'table' => 'svc_broadband',
524         'addl_from' => 'JOIN router USING (routernum)',
525         'hashref' => {},
526         'extra_sql' => 'WHERE svc_broadband.blocknum IS NULL '.
527                        'AND router.manual_addr IS NULL',
528     }) 
529   ) {
530    
531     next SVC if $self->ip_addr eq '';
532     my $NetAddr = $self->NetAddr;
533     # inefficient, but should only need to run once
534     foreach my $block (@all_blocks) {
535       if ($block->NetAddr->contains($NetAddr)) {
536         $self->set(blocknum => $block->blocknum);
537         my $error = $self->replace;
538         warn "WARNING: error assigning blocknum ".$block->blocknum.
539         " to service ".$self->svcnum."\n$error; skipped\n"
540           if $error;
541         next SVC;
542       }
543     }
544     warn "WARNING: no block found containing ".$NetAddr->addr." for service ".
545       $self->svcnum;
546     #next SVC;
547   }
548
549   '';
550 }
551
552 =back
553
554 =head1 BUGS
555
556 The business with sb_field has been 'fixed', in a manner of speaking.
557
558 allowed_routers isn't agent virtualized because part_svc isn't agent
559 virtualized
560
561 Having both routernum and blocknum as foreign keys is somewhat dubious.
562
563 =head1 SEE ALSO
564
565 FS::svc_Common, FS::Record, FS::addr_block,
566 FS::part_svc, schema.html from the base documentation.
567
568 =cut
569
570 1;
571