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