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