make svc_broadband upgrade errors non-fatal
[freeside.git] / FS / FS / svc_broadband.pm
1 package FS::svc_broadband;
2 use base qw(FS::svc_Radius_Mixin FS::svc_Tower_Mixin FS::svc_Common);
3
4 use strict;
5 use vars qw($conf);
6
7 { no warnings 'redefine'; use NetAddr::IP; }
8 use FS::Record qw( qsearchs qsearch dbh );
9 use FS::cust_svc;
10 use FS::addr_block;
11 use FS::part_svc_router;
12 use FS::tower_sector;
13
14 $FS::UID::callback{'FS::svc_broadband'} = sub { 
15   $conf = new FS::Conf;
16 };
17
18 =head1 NAME
19
20 FS::svc_broadband - Object methods for svc_broadband records
21
22 =head1 SYNOPSIS
23
24   use FS::svc_broadband;
25
26   $record = new FS::svc_broadband \%hash;
27   $record = new FS::svc_broadband { 'column' => 'value' };
28
29   $error = $record->insert;
30
31   $error = $new_record->replace($old_record);
32
33   $error = $record->delete;
34
35   $error = $record->check;
36
37   $error = $record->suspend;
38
39   $error = $record->unsuspend;
40
41   $error = $record->cancel;
42
43 =head1 DESCRIPTION
44
45 An FS::svc_broadband object represents a 'broadband' Internet connection, such
46 as a DSL, cable modem, or fixed wireless link.  These services are assumed to
47 have the following properties:
48
49 FS::svc_broadband inherits from FS::svc_Common.  The following fields are
50 currently supported:
51
52 =over 4
53
54 =item svcnum - primary key
55
56 =item blocknum - see FS::addr_block
57
58 =item
59 speed_up - maximum upload speed, in bits per second.  If set to zero, upload
60 speed will be unlimited.  Exports that do traffic shaping should handle this
61 correctly, and not blindly set the upload speed to zero and kill the customer's
62 connection.
63
64 =item
65 speed_down - maximum download speed, as above
66
67 =item ip_addr - the customer's IP address.  If the customer needs more than one
68 IP address, set this to the address of the customer's router.  As a result, the
69 customer's router will have the same address for both its internal and external
70 interfaces thus saving address space.  This has been found to work on most NAT
71 routers available.
72
73 =item plan_id
74
75 =back
76
77 =head1 METHODS
78
79 =over 4
80
81 =item new HASHREF
82
83 Creates a new svc_broadband.  To add the record to the database, see
84 "insert".
85
86 Note that this stores the hash reference, not a distinct copy of the hash it
87 points to.  You can ask the object for a copy with the I<hash> method.
88
89 =cut
90
91 sub table_info {
92   {
93     'name' => 'Wireless broadband',
94     'name_plural' => 'Wireless broadband services',
95     'longname_plural' => 'Fixed wireless broadband services',
96     'display_weight' => 50,
97     'cancel_weight'  => 70,
98     'ip_field' => 'ip_addr',
99     'fields' => {
100       'svcnum'      => 'Service',
101       'description' => 'Descriptive label for this particular device',
102       'speed_down'  => 'Maximum download speed for this service in Kbps.  0 denotes unlimited.',
103       'speed_up'    => 'Maximum upload speed for this service in Kbps.  0 denotes unlimited.',
104       'ip_addr'     => 'IP address.  Leave blank for automatic assignment.',
105       'blocknum'    => 
106       { 'label' => 'Address block',
107                          'type'  => 'select',
108                          'select_table' => 'addr_block',
109                           'select_key'   => 'blocknum',
110                          'select_label' => 'cidr',
111                          'disable_inventory' => 1,
112                        },
113      'plan_id' => 'Service Plan Id',
114      'performance_profile' => 'Peformance Profile',
115      'authkey'      => 'Authentication key',
116      'mac_addr'     => 'MAC address',
117      'latitude'     => 'Latitude',
118      'longitude'    => 'Longitude',
119      'altitude'     => 'Altitude',
120      'vlan_profile' => 'VLAN profile',
121      'sectornum'    => 'Tower/sector',
122      'routernum'    => 'Router/block',
123      'usergroup'    => { 
124                          label => 'RADIUS groups',
125                          type  => 'select-radius_group.html',
126                          #select_table => 'radius_group',
127                          #select_key   => 'groupnum',
128                          #select_label => 'groupname',
129                          disable_inventory => 1,
130                          multiple => 1,
131                        },
132     },
133   };
134 }
135
136 sub table { 'svc_broadband'; }
137
138 sub table_dupcheck_fields { ( 'mac_addr' ); }
139
140 =item search HASHREF
141
142 Class method which returns a qsearch hash expression to search for parameters
143 specified in HASHREF.
144
145 Parameters:
146
147 =over 4
148
149 =item unlinked - set to search for all unlinked services.  Overrides all other options.
150
151 =item agentnum
152
153 =item custnum
154
155 =item svcpart
156
157 =item ip_addr
158
159 =item pkgpart - arrayref
160
161 =item routernum - arrayref
162
163 =item sectornum - arrayref
164
165 =item towernum - arrayref
166
167 =item order_by
168
169 =back
170
171 =cut
172
173 sub search {
174   my ($class, $params) = @_;
175   my @where = ();
176   my @from = (
177     'LEFT JOIN cust_svc  USING ( svcnum  )',
178     'LEFT JOIN part_svc  USING ( svcpart )',
179     'LEFT JOIN cust_pkg  USING ( pkgnum  )',
180     'LEFT JOIN cust_main USING ( custnum )',
181   );
182
183   # based on FS::svc_acct::search, probably the most mature of the bunch
184   #unlinked
185   push @where, 'pkgnum IS NULL' if $params->{'unlinked'};
186   
187   #agentnum
188   if ( $params->{'agentnum'} =~ /^(\d+)$/ and $1 ) {
189     push @where, "cust_main.agentnum = $1";
190   }
191   push @where, $FS::CurrentUser::CurrentUser->agentnums_sql(
192     'null_right' => 'View/link unlinked services',
193     'table' => 'cust_main'
194   );
195
196   #custnum
197   if ( $params->{'custnum'} =~ /^(\d+)$/ and $1 ) {
198     push @where, "custnum = $1";
199   }
200
201   #pkgpart, now properly untainted, can be arrayref
202   for my $pkgpart ( $params->{'pkgpart'} ) {
203     if ( ref $pkgpart ) {
204       my $where = join(',', map { /^(\d+)$/ ? $1 : () } @$pkgpart );
205       push @where, "cust_pkg.pkgpart IN ($where)" if $where;
206     }
207     elsif ( $pkgpart =~ /^(\d+)$/ ) {
208       push @where, "cust_pkg.pkgpart = $1";
209     }
210   }
211
212   #routernum, can be arrayref
213   for my $routernum ( $params->{'routernum'} ) {
214     # this no longer uses addr_block
215     if ( ref $routernum and grep { $_ } @$routernum ) {
216       my $in = join(',', map { /^(\d+)$/ ? $1 : () } @$routernum );
217       my @orwhere;
218       push @orwhere, "svc_broadband.routernum IN ($in)" if $in;
219       push @orwhere, "svc_broadband.routernum IS NULL" 
220         if grep /^none$/, @$routernum;
221       push @where, '( '.join(' OR ', @orwhere).' )';
222     }
223     elsif ( $routernum =~ /^(\d+)$/ ) {
224       push @where, "svc_broadband.routernum = $1";
225     }
226     elsif ( $routernum eq 'none' ) {
227       push @where, "svc_broadband.routernum IS NULL";
228     }
229   }
230
231   #sector and tower, as above
232   my @where_sector = $class->tower_sector_sql($params);
233   if ( @where_sector ) {
234     push @where, @where_sector;
235     push @from, 'LEFT JOIN tower_sector USING ( sectornum )';
236   }
237  
238   #svcnum
239   if ( $params->{'svcnum'} =~ /^(\d+)$/ ) {
240     push @where, "svcnum = $1";
241   }
242
243   #svcpart
244   if ( $params->{'svcpart'} =~ /^(\d+)$/ ) {
245     push @where, "svcpart = $1";
246   }
247
248   #ip_addr
249   if ( $params->{'ip_addr'} =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/ ) {
250     push @where, "ip_addr = '$1'";
251   }
252
253   #custnum
254   if ( $params->{'custnum'} =~ /^(\d+)$/ and $1) {
255     push @where, "custnum = $1";
256   }
257   
258   my $addl_from = join(' ', @from);
259   my $extra_sql = '';
260   $extra_sql = 'WHERE '.join(' AND ', @where) if @where;
261   my $count_query = "SELECT COUNT(*) FROM svc_broadband $addl_from $extra_sql";
262   return( {
263       'table'   => 'svc_broadband',
264       'hashref' => {},
265       'select'  => join(', ',
266         'svc_broadband.*',
267         'part_svc.svc',
268         'cust_main.custnum',
269         FS::UI::Web::cust_sql_fields($params->{'cust_fields'}),
270       ),
271       'extra_sql' => $extra_sql,
272       'addl_from' => $addl_from,
273       'order_by'  => "ORDER BY ".($params->{'order_by'} || 'svcnum'),
274       'count_query' => $count_query,
275     } );
276 }
277
278 =item search_sql STRING
279
280 Class method which returns an SQL fragment to search for the given string.
281
282 =cut
283
284 sub search_sql {
285   my( $class, $string ) = @_;
286   if ( $string =~ /^(\d{1,3}\.){3}\d{1,3}$/ ) {
287     $class->search_sql_field('ip_addr', $string );
288   }elsif ( $string =~ /^([a-fA-F0-9]{12})$/ ) {
289     $class->search_sql_field('mac_addr', uc($string));
290   }elsif ( $string =~ /^(([a-fA-F0-9]{1,2}:){5}([a-fA-F0-9]{1,2}))$/ ) {
291     $class->search_sql_field('mac_addr', uc("$2$3$4$5$6$7") );
292   } else {
293     '1 = 0'; #false
294   }
295 }
296
297 =item label
298
299 Returns the IP address.
300
301 =cut
302
303 sub label {
304   my $self = shift;
305   $self->ip_addr;
306 }
307
308 =item insert [ , OPTION => VALUE ... ]
309
310 Adds this record to the database.  If there is an error, returns the error,
311 otherwise returns false.
312
313 The additional fields pkgnum and svcpart (see FS::cust_svc) should be 
314 defined.  An FS::cust_svc record will be created and inserted.
315
316 Currently available options are: I<depend_jobnum>
317
318 If I<depend_jobnum> is set (to a scalar jobnum or an array reference of
319 jobnums), all provisioning jobs will have a dependancy on the supplied
320 jobnum(s) (they will not run until the specific job(s) complete(s)).
321
322 =cut
323
324 # Standard FS::svc_Common::insert
325
326 =item delete
327
328 Delete this record from the database.
329
330 =cut
331
332 # Standard FS::svc_Common::delete
333
334 =item replace OLD_RECORD
335
336 Replaces the OLD_RECORD with this one in the database.  If there is an error,
337 returns the error, otherwise returns false.
338
339 # Standard FS::svc_Common::replace
340
341 =item suspend
342
343 Called by the suspend method of FS::cust_pkg (see FS::cust_pkg).
344
345 =item unsuspend
346
347 Called by the unsuspend method of FS::cust_pkg (see FS::cust_pkg).
348
349 =item cancel
350
351 Called by the cancel method of FS::cust_pkg (see FS::cust_pkg).
352
353 =item check
354
355 Checks all fields to make sure this is a valid broadband service.  If there is
356 an error, returns the error, otherwise returns false.  Called by the insert
357 and replace methods.
358
359 =cut
360
361 sub check {
362   my $self = shift;
363   my $x = $self->setfixed;
364
365   return $x unless ref($x);
366
367   # remove delimiters
368   my $mac_addr = uc($self->get('mac_addr'));
369   $mac_addr =~ s/[-: ]//g;
370   $self->set('mac_addr', $mac_addr);
371
372   my $error =
373     $self->ut_numbern('svcnum')
374     || $self->ut_numbern('blocknum')
375     || $self->ut_foreign_keyn('routernum', 'router', 'routernum')
376     || $self->ut_foreign_keyn('sectornum', 'tower_sector', 'sectornum')
377     || $self->ut_textn('description')
378     || $self->ut_numbern('speed_up')
379     || $self->ut_numbern('speed_down')
380     || $self->ut_ipn('ip_addr')
381     || $self->ut_hexn('mac_addr')
382     || $self->ut_hexn('auth_key')
383     || $self->ut_coordn('latitude')
384     || $self->ut_coordn('longitude')
385     || $self->ut_sfloatn('altitude')
386     || $self->ut_textn('vlan_profile')
387     || $self->ut_textn('plan_id')
388   ;
389   return $error if $error;
390
391   if(($self->speed_up || 0) < 0) { return 'speed_up must be positive'; }
392   if(($self->speed_down || 0) < 0) { return 'speed_down must be positive'; }
393
394   my $cust_svc = $self->svcnum
395                  ? qsearchs('cust_svc', { 'svcnum' => $self->svcnum } )
396                  : '';
397   my $cust_pkg;
398   my $svcpart;
399   if ($cust_svc) {
400     $cust_pkg = $cust_svc->cust_pkg;
401     $svcpart = $cust_svc->svcpart;
402   }else{
403     $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $self->pkgnum } );
404     return "Invalid pkgnum" unless $cust_pkg;
405     $svcpart = $self->svcpart;
406   }
407   my $agentnum = $cust_pkg->cust_main->agentnum if $cust_pkg;
408
409   if ($self->routernum) {
410     return "Router ".$self->routernum." does not provide this service"
411       unless qsearchs('part_svc_router', { 
412         svcpart => $svcpart,
413         routernum => $self->routernum
414     });
415   
416     my $router = $self->router;
417     return "Router ".$self->routernum." does not serve this customer"
418       if $router->agentnum and $router->agentnum != $agentnum;
419
420     if ( $router->auto_addr ) {
421       my $addr_block = $self->addr_block;
422       unless ( $addr_block and $addr_block->manual_flag ) {
423         my $error = $self->assign_ip_addr;
424         return $error if $error;
425       }
426     }
427     else {
428       $self->blocknum('');
429     }
430   } # if $self->routernum
431
432   if ( $cust_pkg && ! $self->latitude && ! $self->longitude ) {
433     my $l = $cust_pkg->cust_location_or_main;
434     if ( $l->ship_latitude && $l->ship_longitude ) {
435       $self->latitude(  $l->ship_latitude  );
436       $self->longitude( $l->ship_longitude );
437     } elsif ( $l->latitude && $l->longitude ) {
438       $self->latitude(  $l->latitude  );
439       $self->longitude( $l->longitude );
440     }
441   }
442
443   $error = $self->_check_ip_addr;
444   return $error if $error;
445
446   $self->SUPER::check;
447 }
448
449 =item assign_ip_addr
450
451 Assign an address block matching the selected router, and the selected block
452 if there is one.
453
454 =cut
455
456 sub assign_ip_addr {
457   my $self = shift;
458   my @blocks;
459   my $ip_addr;
460
461   if ( $self->blocknum and $self->addr_block->routernum == $self->routernum ) {
462     # simple case: user chose a block, find an address in that block
463     # (this overrides an existing IP address if it's not in the block)
464     @blocks = ($self->addr_block);
465   }
466   elsif ( $self->routernum ) {
467     @blocks = $self->router->auto_addr_block;
468   }
469   else { 
470     return '';
471   }
472
473   foreach my $block ( @blocks ) {
474     if ( $self->ip_addr and $block->NetAddr->contains($self->NetAddr) ) {
475       # don't change anything
476       return '';
477     }
478     $ip_addr = $block->next_free_addr;
479     last if $ip_addr;
480   }
481   if ( $ip_addr ) {
482     $self->set(ip_addr => $ip_addr->addr);
483     return '';
484   }
485   else {
486     return 'No IP address available on this router';
487   }
488 }
489
490 sub _check_ip_addr {
491   my $self = shift;
492
493   if (not($self->ip_addr) or $self->ip_addr eq '0.0.0.0') {
494     return '' if $conf->exists('svc_broadband-allow_null_ip_addr'); 
495     return 'IP address required';
496   }
497 #  if (my $dup = qsearchs('svc_broadband', {
498 #        ip_addr => $self->ip_addr,
499 #        svcnum  => {op=>'!=', value => $self->svcnum}
500 #      }) ) {
501 #    return 'IP address conflicts with svcnum '.$dup->svcnum;
502 #  }
503   '';
504 }
505
506 sub _check_duplicate {
507   my $self = shift;
508
509   return "MAC already in use"
510     if ( $self->mac_addr &&
511          scalar( qsearch( 'svc_broadband', { 'mac_addr', $self->mac_addr } ) )
512        );
513
514   '';
515 }
516
517
518 =item NetAddr
519
520 Returns a NetAddr::IP object containing the IP address of this service.  The netmask 
521 is /32.
522
523 =cut
524
525 sub NetAddr {
526   my $self = shift;
527   new NetAddr::IP ($self->ip_addr);
528 }
529
530 =item addr_block
531
532 Returns the FS::addr_block record (i.e. the address block) for this broadband service.
533
534 =cut
535
536 sub addr_block {
537   my $self = shift;
538   qsearchs('addr_block', { blocknum => $self->blocknum });
539 }
540
541 =item router
542
543 Returns the FS::router record for this service.
544
545 =cut
546
547 sub router {
548   my $self = shift;
549   qsearchs('router', { routernum => $self->routernum });
550 }
551
552 =item allowed_routers
553
554 Returns a list of allowed FS::router objects.
555
556 =cut
557
558 sub allowed_routers {
559   my $self = shift;
560   my $svcpart = $self->svcnum ? $self->cust_svc->svcpart : $self->svcpart;
561   map { $_->router } qsearch('part_svc_router', 
562     { svcpart => $self->cust_svc->svcpart });
563 }
564
565 =back
566
567
568 =item mac_addr_formatted CASE DELIMITER
569
570 Format the MAC address (for use by exports).  If CASE starts with "l"
571 (for "lowercase"), it's returned in lowercase.  DELIMITER is inserted
572 between octets.
573
574 =cut
575
576 sub mac_addr_formatted {
577   my $self = shift;
578   my ($case, $delim) = @_;
579   my $addr = $self->mac_addr;
580   $addr = lc($addr) if $case =~ /^l/i;
581   join( $delim || '', $addr =~ /../g );
582 }
583
584 #class method
585 sub _upgrade_data {
586   my $class = shift;
587
588   local($FS::svc_Common::noexport_hack) = 1;
589
590   # set routernum to addr_block.routernum
591   foreach my $self (qsearch('svc_broadband', {
592       blocknum => {op => '!=', value => ''},
593       routernum => ''
594     })) {
595     my $addr_block = $self->addr_block;
596     my $ip_addr = $self->ip_addr;
597     my $routernum = $addr_block->routernum;
598     if ( $routernum ) {
599       $self->set(routernum => $routernum);
600       my $error = $self->check;
601       # sanity check: don't allow this to change IP address or block
602       # (other than setting blocknum to null for a non-auto-assigned router)
603       if ( $self->ip_addr ne $ip_addr 
604         or ($self->blocknum and $self->blocknum != $addr_block->blocknum)) {
605         warn "WARNING: Upgrading service ".$self->svcnum." would change its block/address; skipped.\n";
606         next;
607       }
608
609       $error ||= $self->replace;
610       die "error assigning routernum $routernum to service ".$self->svcnum.
611           ":\n$error\n"
612         if $error;
613     }
614     else {
615       warn "svcnum ".$self->svcnum.
616         ": no routernum in address block ".$addr_block->cidr.", skipped\n";
617     }
618   }
619   '';
620 }
621
622 =back
623
624 =head1 BUGS
625
626 The business with sb_field has been 'fixed', in a manner of speaking.
627
628 allowed_routers isn't agent virtualized because part_svc isn't agent
629 virtualized
630
631 Having both routernum and blocknum as foreign keys is somewhat dubious.
632
633 =head1 SEE ALSO
634
635 FS::svc_Common, FS::Record, FS::addr_block,
636 FS::part_svc, schema.html from the base documentation.
637
638 =cut
639
640 1;
641