Merge branch 'patch-19' of https://github.com/gjones2/Freeside
[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-fA-F0-9]{12})$/ ) {
238     $class->search_sql_field('mac_addr', uc($string));
239   } elsif ( $string =~ /^(([a-fA-F0-9]{1,2}:){5}([a-fA-F0-9]{1,2}))$/ ) {
240     $class->search_sql_field('mac_addr', uc("$2$3$4$5$6$7") );
241   } elsif ( $string =~ /^(\d+)$/ ) {
242     my $table = $class->table;
243     "$table.svcnum = $1";
244   } else {
245     '1 = 0'; #false
246   }
247 }
248
249 =item smart_search STRING
250
251 =cut
252
253 sub smart_search {
254   my( $class, $string ) = @_;
255   qsearch({
256     'table'     => $class->table, #'svc_broadband',
257     'hashref'   => {},
258     'extra_sql' => 'WHERE '. $class->search_sql($string),
259   });
260 }
261
262 =item label
263
264 Returns the IP address.
265
266 =cut
267
268 sub label {
269   my $self = shift;
270   my $label = 'IP:'. ($self->ip_addr || 'Unknown');
271   $label .= ', MAC:'. $self->mac_addr
272     if $self->mac_addr;
273   $label .= ' ('. $self->description. ')'
274     if $self->description;
275   return $label;
276 }
277
278 =item insert [ , OPTION => VALUE ... ]
279
280 Adds this record to the database.  If there is an error, returns the error,
281 otherwise returns false.
282
283 The additional fields pkgnum and svcpart (see FS::cust_svc) should be 
284 defined.  An FS::cust_svc record will be created and inserted.
285
286 Currently available options are: I<depend_jobnum>
287
288 If I<depend_jobnum> is set (to a scalar jobnum or an array reference of
289 jobnums), all provisioning jobs will have a dependancy on the supplied
290 jobnum(s) (they will not run until the specific job(s) complete(s)).
291
292 =cut
293
294 # Standard FS::svc_Common::insert
295
296 =item delete
297
298 Delete this record from the database.
299
300 =cut
301
302 # Standard FS::svc_Common::delete
303
304 =item replace OLD_RECORD
305
306 Replaces the OLD_RECORD with this one in the database.  If there is an error,
307 returns the error, otherwise returns false.
308
309 # Standard FS::svc_Common::replace
310
311 =item suspend
312
313 Called by the suspend method of FS::cust_pkg (see FS::cust_pkg).
314
315 =item unsuspend
316
317 Called by the unsuspend method of FS::cust_pkg (see FS::cust_pkg).
318
319 =item cancel
320
321 Called by the cancel method of FS::cust_pkg (see FS::cust_pkg).
322
323 =item check
324
325 Checks all fields to make sure this is a valid broadband service.  If there is
326 an error, returns the error, otherwise returns false.  Called by the insert
327 and replace methods.
328
329 =cut
330
331 sub check {
332   my $self = shift;
333   my $x = $self->setfixed;
334
335   return $x unless ref($x);
336
337   # remove delimiters
338   my $mac_addr = uc($self->get('mac_addr'));
339   $mac_addr =~ s/[\W_]//g;
340   $self->set('mac_addr', $mac_addr);
341
342   my $error =
343     $self->ut_numbern('svcnum')
344     || $self->ut_numbern('blocknum')
345     || $self->ut_foreign_keyn('routernum', 'router', 'routernum')
346     || $self->ut_foreign_keyn('sectornum', 'tower_sector', 'sectornum')
347     || $self->ut_textn('description')
348     || $self->ut_numbern('speed_up')
349     || $self->ut_numbern('speed_down')
350     || $self->ut_ipn('ip_addr')
351     || $self->ut_hexn('mac_addr')
352     || $self->ut_hexn('auth_key')
353     || $self->ut_coordn('latitude')
354     || $self->ut_coordn('longitude')
355     || $self->ut_sfloatn('altitude')
356     || $self->ut_textn('vlan_profile')
357     || $self->ut_textn('plan_id')
358     || $self->ut_alphan('radio_serialnum')
359     || $self->ut_textn('radio_location')
360     || $self->ut_textn('poe_location')
361     || $self->ut_snumbern('rssi')
362     || $self->ut_numbern('suid')
363     || $self->ut_foreign_keyn('shared_svcnum', 'svc_broadband', 'svcnum')
364   ;
365   return $error if $error;
366
367   if(($self->speed_up || 0) < 0) { return 'speed_up must be positive'; }
368   if(($self->speed_down || 0) < 0) { return 'speed_down must be positive'; }
369
370   my $cust_svc = $self->svcnum
371                  ? qsearchs('cust_svc', { 'svcnum' => $self->svcnum } )
372                  : '';
373   my $cust_pkg;
374   my $svcpart;
375   if ($cust_svc) {
376     $cust_pkg = $cust_svc->cust_pkg;
377     $svcpart = $cust_svc->svcpart;
378   }else{
379     $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $self->pkgnum } );
380     return "Invalid pkgnum" unless $cust_pkg;
381     $svcpart = $self->svcpart;
382   }
383   my $agentnum = $cust_pkg->cust_main->agentnum if $cust_pkg;
384
385   # assign IP address / router / block
386   $error = $self->svc_ip_check;
387   return $error if $error;
388   if ( !$self->ip_addr 
389        and !$conf->exists('svc_broadband-allow_null_ip_addr') ) {
390     return 'IP address is required';
391   }
392
393   if ( $cust_pkg && ! $self->latitude && ! $self->longitude ) {
394     my $l = $cust_pkg->cust_location_or_main;
395     if ( $l->ship_latitude && $l->ship_longitude ) {
396       $self->latitude(  $l->ship_latitude  );
397       $self->longitude( $l->ship_longitude );
398     } elsif ( $l->latitude && $l->longitude ) {
399       $self->latitude(  $l->latitude  );
400       $self->longitude( $l->longitude );
401     }
402   }
403
404   $self->SUPER::check;
405 }
406
407 sub _check_duplicate {
408   my $self = shift;
409   # Not a reliable check because the table isn't locked, but 
410   # that's why we have a unique index.  This is just to give a
411   # friendlier error message.
412   my @dup;
413   @dup = $self->find_duplicates('global', 'mac_addr');
414   if ( @dup ) {
415     return "MAC address in use (svcnum ".$dup[0]->svcnum.")";
416   }
417
418   '';
419 }
420
421 =item mac_addr_formatted CASE DELIMITER
422
423 Format the MAC address (for use by exports).  If CASE starts with "l"
424 (for "lowercase"), it's returned in lowercase.  DELIMITER is inserted
425 between octets.
426
427 =cut
428
429 sub mac_addr_formatted {
430   my $self = shift;
431   my ($case, $delim) = @_;
432   my $addr = $self->mac_addr;
433   $addr = lc($addr) if $case =~ /^l/i;
434   join( $delim || '', $addr =~ /../g );
435 }
436
437 #class method
438 sub _upgrade_data {
439   my $class = shift;
440
441   local($FS::svc_Common::noexport_hack) = 1;
442
443   # set routernum to addr_block.routernum
444   foreach my $self (qsearch('svc_broadband', {
445       blocknum => {op => '!=', value => ''},
446       routernum => ''
447     })) {
448     my $addr_block = $self->addr_block;
449     if ( !$addr_block ) {
450       # super paranoid mode
451       warn "WARNING: svcnum ".$self->svcnum." is assigned to addr_block ".$self->blocknum.", which does not exist; skipped.\n";
452       next;
453     }
454     my $ip_addr = $self->ip_addr;
455     my $routernum = $addr_block->routernum;
456     if ( $routernum ) {
457       $self->set(routernum => $routernum);
458       my $error = $self->check;
459       # sanity check: don't allow this to change IP address or block
460       # (other than setting blocknum to null for a non-auto-assigned router)
461       if ( $self->ip_addr ne $ip_addr 
462         or ($self->blocknum and $self->blocknum != $addr_block->blocknum)) {
463         warn "WARNING: Upgrading service ".$self->svcnum." would change its block/address; skipped.\n";
464         next;
465       }
466
467       $error ||= $self->replace;
468       warn "WARNING: error assigning routernum $routernum to service ".$self->svcnum.
469           ":\n$error; skipped\n"
470         if $error;
471     }
472     else {
473       warn "svcnum ".$self->svcnum.
474         ": no routernum in address block ".$addr_block->cidr.", skipped\n";
475     }
476   }
477
478   # assign blocknums to services that should have them
479   my @all_blocks = qsearch('addr_block', { });
480   SVC: foreach my $self ( 
481     qsearch({
482         'select' => 'svc_broadband.*',
483         'table' => 'svc_broadband',
484         'addl_from' => 'JOIN router USING (routernum)',
485         'hashref' => {},
486         'extra_sql' => 'WHERE svc_broadband.blocknum IS NULL '.
487                        'AND router.manual_addr IS NULL',
488     }) 
489   ) {
490    
491     next SVC if $self->ip_addr eq '';
492     my $NetAddr = $self->NetAddr;
493     # inefficient, but should only need to run once
494     foreach my $block (@all_blocks) {
495       if ($block->NetAddr->contains($NetAddr)) {
496         $self->set(blocknum => $block->blocknum);
497         my $error = $self->replace;
498         warn "WARNING: error assigning blocknum ".$block->blocknum.
499         " to service ".$self->svcnum."\n$error; skipped\n"
500           if $error;
501         next SVC;
502       }
503     }
504     warn "WARNING: no block found containing ".$NetAddr->addr." for service ".
505       $self->svcnum;
506     #next SVC;
507   }
508
509   '';
510 }
511
512 =back
513
514 =head1 BUGS
515
516 The business with sb_field has been 'fixed', in a manner of speaking.
517
518 allowed_routers isn't agent virtualized because part_svc isn't agent
519 virtualized
520
521 Having both routernum and blocknum as foreign keys is somewhat dubious.
522
523 =head1 SEE ALSO
524
525 FS::svc_Common, FS::Record, FS::addr_block,
526 FS::part_svc, schema.html from the base documentation.
527
528 =cut
529
530 1;
531