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