broadband_sql export, #15924
[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 $error = $self->assign_ip_addr;
422       return $error if $error;
423     }
424     else {
425       $self->blocknum('');
426     }
427   } # if $self->routernum
428
429   if ( $cust_pkg && ! $self->latitude && ! $self->longitude ) {
430     my $l = $cust_pkg->cust_location_or_main;
431     if ( $l->ship_latitude && $l->ship_longitude ) {
432       $self->latitude(  $l->ship_latitude  );
433       $self->longitude( $l->ship_longitude );
434     } elsif ( $l->latitude && $l->longitude ) {
435       $self->latitude(  $l->latitude  );
436       $self->longitude( $l->longitude );
437     }
438   }
439
440   $error = $self->_check_ip_addr;
441   return $error if $error;
442
443   $self->SUPER::check;
444 }
445
446 =item assign_ip_addr
447
448 Assign an address block matching the selected router, and the selected block
449 if there is one.
450
451 =cut
452
453 sub assign_ip_addr {
454   my $self = shift;
455   my @blocks;
456   my $ip_addr;
457
458   if ( $self->blocknum and $self->addr_block->routernum == $self->routernum ) {
459     # simple case: user chose a block, find an address in that block
460     # (this overrides an existing IP address if it's not in the block)
461     @blocks = ($self->addr_block);
462   }
463   elsif ( $self->routernum ) {
464     @blocks = $self->router->auto_addr_block;
465   }
466   else { 
467     return '';
468   }
469
470   foreach my $block ( @blocks ) {
471     if ( $self->ip_addr and $block->NetAddr->contains($self->NetAddr) ) {
472       # don't change anything
473       return '';
474     }
475     $ip_addr = $block->next_free_addr;
476     last if $ip_addr;
477   }
478   if ( $ip_addr ) {
479     $self->set(ip_addr => $ip_addr->addr);
480     return '';
481   }
482   else {
483     return 'No IP address available on this router';
484   }
485 }
486
487 sub _check_ip_addr {
488   my $self = shift;
489
490   if (not($self->ip_addr) or $self->ip_addr eq '0.0.0.0') {
491     return '' if $conf->exists('svc_broadband-allow_null_ip_addr'); 
492     return 'IP address required';
493   }
494 #  if (my $dup = qsearchs('svc_broadband', {
495 #        ip_addr => $self->ip_addr,
496 #        svcnum  => {op=>'!=', value => $self->svcnum}
497 #      }) ) {
498 #    return 'IP address conflicts with svcnum '.$dup->svcnum;
499 #  }
500   '';
501 }
502
503 sub _check_duplicate {
504   my $self = shift;
505
506   return "MAC already in use"
507     if ( $self->mac_addr &&
508          scalar( qsearch( 'svc_broadband', { 'mac_addr', $self->mac_addr } ) )
509        );
510
511   '';
512 }
513
514
515 =item NetAddr
516
517 Returns a NetAddr::IP object containing the IP address of this service.  The netmask 
518 is /32.
519
520 =cut
521
522 sub NetAddr {
523   my $self = shift;
524   new NetAddr::IP ($self->ip_addr);
525 }
526
527 =item addr_block
528
529 Returns the FS::addr_block record (i.e. the address block) for this broadband service.
530
531 =cut
532
533 sub addr_block {
534   my $self = shift;
535   qsearchs('addr_block', { blocknum => $self->blocknum });
536 }
537
538 =item router
539
540 Returns the FS::router record for this service.
541
542 =cut
543
544 sub router {
545   my $self = shift;
546   qsearchs('router', { routernum => $self->routernum });
547 }
548
549 =item allowed_routers
550
551 Returns a list of allowed FS::router objects.
552
553 =cut
554
555 sub allowed_routers {
556   my $self = shift;
557   my $svcpart = $self->svcnum ? $self->cust_svc->svcpart : $self->svcpart;
558   map { $_->router } qsearch('part_svc_router', 
559     { svcpart => $self->cust_svc->svcpart });
560 }
561
562 =back
563
564
565 =item mac_addr_formatted CASE DELIMITER
566
567 Format the MAC address (for use by exports).  If CASE starts with "l"
568 (for "lowercase"), it's returned in lowercase.  DELIMITER is inserted
569 between octets.
570
571 =cut
572
573 sub mac_addr_formatted {
574   my $self = shift;
575   my ($case, $delim) = @_;
576   my $addr = $self->mac_addr;
577   $addr = lc($addr) if $case =~ /^l/i;
578   join( $delim || '', $addr =~ /../g );
579 }
580
581 =back
582
583
584 #class method
585 sub _upgrade_data {
586   my $class = shift;
587
588   # set routernum to addr_block.routernum
589   foreach my $self (qsearch('svc_broadband', {
590       blocknum => {op => '!=', value => ''},
591       routernum => ''
592     })) {
593     my $addr_block = $self->addr_block;
594     if ( my $routernum = $addr_block->routernum ) {
595       $self->set(routernum => $routernum);
596       my $error = $self->replace;
597       die "error assigning routernum $routernum to service ".$self->svcnum.
598           ":\n$error\n"
599         if $error;
600     }
601     else {
602       warn "svcnum ".$self->svcnum.
603         ": no routernum in address block ".$addr_block->cidr.", skipped\n";
604     }
605   }
606   '';
607 }
608
609 =head1 BUGS
610
611 The business with sb_field has been 'fixed', in a manner of speaking.
612
613 allowed_routers isn't agent virtualized because part_svc isn't agent
614 virtualized
615
616 Having both routernum and blocknum as foreign keys is somewhat dubious.
617
618 =head1 SEE ALSO
619
620 FS::svc_Common, FS::Record, FS::addr_block,
621 FS::part_svc, schema.html from the base documentation.
622
623 =cut
624
625 1;
626