RT# 78356 - created import format for broadband service
[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'      => 'Speed test download (Kbps)',
160       'speed_test_down'    => 'Speed test upload (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_ipn('ip_addr')
368     || $self->ut_hexn('mac_addr')
369     || $self->ut_hexn('auth_key')
370     || $self->ut_coordn('latitude')
371     || $self->ut_coordn('longitude')
372     || $self->ut_sfloatn('altitude')
373     || $self->ut_textn('vlan_profile')
374     || $self->ut_textn('plan_id')
375     || $self->ut_alphan('radio_serialnum')
376     || $self->ut_textn('radio_location')
377     || $self->ut_textn('poe_location')
378     || $self->ut_snumbern('rssi')
379     || $self->ut_numbern('suid')
380     || $self->ut_foreign_keyn('shared_svcnum', 'svc_broadband', 'svcnum')
381     || $self->ut_textn('serviceid') #too lenient?
382   ;
383   return $error if $error;
384
385   if(($self->speed_up || 0) < 0) { return 'speed_up must be positive'; }
386   if(($self->speed_down || 0) < 0) { return 'speed_down must be positive'; }
387
388   my $cust_svc = $self->svcnum
389                  ? qsearchs('cust_svc', { 'svcnum' => $self->svcnum } )
390                  : '';
391   my $cust_pkg;
392   my $svcpart;
393   if ($cust_svc) {
394     $cust_pkg = $cust_svc->cust_pkg;
395     $svcpart = $cust_svc->svcpart;
396   }else{
397     $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $self->pkgnum } );
398     return "Invalid pkgnum" unless $cust_pkg;
399     $svcpart = $self->svcpart;
400   }
401   my $agentnum = $cust_pkg->cust_main->agentnum if $cust_pkg;
402
403   # assign IP address / router / block
404   $error = $self->svc_ip_check;
405   return $error if $error;
406   if ( !$self->ip_addr 
407        and !$conf->exists('svc_broadband-allow_null_ip_addr') ) {
408     return 'IP address is required';
409   }
410
411   if ( $cust_pkg && ! $self->latitude && ! $self->longitude ) {
412     my $l = $cust_pkg->cust_location_or_main;
413     if ( $l->ship_latitude && $l->ship_longitude ) {
414       $self->latitude(  $l->ship_latitude  );
415       $self->longitude( $l->ship_longitude );
416     } elsif ( $l->latitude && $l->longitude ) {
417       $self->latitude(  $l->latitude  );
418       $self->longitude( $l->longitude );
419     }
420   }
421
422   $self->SUPER::check;
423 }
424
425 sub _check_duplicate {
426   my $self = shift;
427   # Not a reliable check because the table isn't locked, but 
428   # that's why we have a unique index.  This is just to give a
429   # friendlier error message.
430   my @dup;
431   @dup = $self->find_duplicates('global', 'mac_addr');
432   if ( @dup ) {
433     return "MAC address in use (svcnum ".$dup[0]->svcnum.")";
434   }
435
436   '';
437 }
438
439 #class method
440 sub _upgrade_data {
441   my $class = shift;
442
443   local($FS::svc_Common::noexport_hack) = 1;
444
445   # fix wrong-case MAC addresses
446   my $dbh = dbh;
447   $dbh->do('UPDATE svc_broadband SET mac_addr = UPPER(mac_addr);')
448     or die $dbh->errstr;
449
450   # set routernum to addr_block.routernum
451   foreach my $self (qsearch('svc_broadband', {
452       blocknum => {op => '!=', value => ''},
453       routernum => ''
454     })) {
455     my $addr_block = $self->addr_block;
456     if ( !$addr_block ) {
457       # super paranoid mode
458       warn "WARNING: svcnum ".$self->svcnum." is assigned to addr_block ".$self->blocknum.", which does not exist; skipped.\n";
459       next;
460     }
461     my $ip_addr = $self->ip_addr;
462     my $routernum = $addr_block->routernum;
463     if ( $routernum ) {
464       $self->set(routernum => $routernum);
465       my $error = $self->check;
466       # sanity check: don't allow this to change IP address or block
467       # (other than setting blocknum to null for a non-auto-assigned router)
468       if ( $self->ip_addr ne $ip_addr 
469         or ($self->blocknum and $self->blocknum != $addr_block->blocknum)) {
470         warn "WARNING: Upgrading service ".$self->svcnum." would change its block/address; skipped.\n";
471         next;
472       }
473
474       $error ||= $self->replace;
475       warn "WARNING: error assigning routernum $routernum to service ".$self->svcnum.
476           ":\n$error; skipped\n"
477         if $error;
478     }
479     else {
480       warn "svcnum ".$self->svcnum.
481         ": no routernum in address block ".$addr_block->cidr.", skipped\n";
482     }
483   }
484
485   # assign blocknums to services that should have them
486   my @all_blocks = qsearch('addr_block', { });
487   SVC: foreach my $self ( 
488     qsearch({
489         'select' => 'svc_broadband.*',
490         'table' => 'svc_broadband',
491         'addl_from' => 'JOIN router USING (routernum)',
492         'hashref' => {},
493         'extra_sql' => 'WHERE svc_broadband.blocknum IS NULL '.
494                        'AND router.manual_addr IS NULL',
495     }) 
496   ) {
497    
498     next SVC if $self->ip_addr eq '';
499     my $NetAddr = $self->NetAddr;
500     # inefficient, but should only need to run once
501     foreach my $block (@all_blocks) {
502       if ($block->NetAddr->contains($NetAddr)) {
503         $self->set(blocknum => $block->blocknum);
504         my $error = $self->replace;
505         warn "WARNING: error assigning blocknum ".$block->blocknum.
506         " to service ".$self->svcnum."\n$error; skipped\n"
507           if $error;
508         next SVC;
509       }
510     }
511     warn "WARNING: no block found containing ".$NetAddr->addr." for service ".
512       $self->svcnum;
513     #next SVC;
514   }
515
516   require FS::Misc::FixIPFormat;
517   FS::Misc::FixIPFormat::fix_bad_addresses_in_table(
518       'svc_broadband', 'svcnum', 'ip_addr',
519   );
520
521   '';
522 }
523
524 =back
525
526 =head1 BUGS
527
528 The business with sb_field has been 'fixed', in a manner of speaking.
529
530 allowed_routers isn't agent virtualized because part_svc isn't agent
531 virtualized
532
533 Having both routernum and blocknum as foreign keys is somewhat dubious.
534
535 =head1 SEE ALSO
536
537 FS::svc_Common, FS::Record, FS::addr_block,
538 FS::part_svc, schema.html from the base documentation.
539
540 =cut
541
542 1;