cleanup
[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       'sectornum'   => 'Tower sector',
106       'blocknum'    => { '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      'usergroup'    => { 
122                          label => 'RADIUS groups',
123                          type  => 'select-radius_group.html',
124                          #select_table => 'radius_group',
125                          #select_key   => 'groupnum',
126                          #select_label => 'groupname',
127                          disable_inventory => 1,
128                          multiple => 1,
129                        },
130     },
131   };
132 }
133
134 sub table { 'svc_broadband'; }
135
136 sub table_dupcheck_fields { ( 'mac_addr' ); }
137
138 =item search HASHREF
139
140 Class method which returns a qsearch hash expression to search for parameters
141 specified in HASHREF.
142
143 Parameters:
144
145 =over 4
146
147 =item unlinked - set to search for all unlinked services.  Overrides all other options.
148
149 =item agentnum
150
151 =item custnum
152
153 =item svcpart
154
155 =item ip_addr
156
157 =item pkgpart - arrayref
158
159 =item routernum - arrayref
160
161 =item sectornum - arrayref
162
163 =item towernum - arrayref
164
165 =item order_by
166
167 =back
168
169 =cut
170
171 sub search {
172   my ($class, $params) = @_;
173   my @where = ();
174   my @from = (
175     'LEFT JOIN cust_svc  USING ( svcnum  )',
176     'LEFT JOIN part_svc  USING ( svcpart )',
177     'LEFT JOIN cust_pkg  USING ( pkgnum  )',
178     'LEFT JOIN cust_main USING ( custnum )',
179   );
180
181   # based on FS::svc_acct::search, probably the most mature of the bunch
182   #unlinked
183   push @where, 'pkgnum IS NULL' if $params->{'unlinked'};
184   
185   #agentnum
186   if ( $params->{'agentnum'} =~ /^(\d+)$/ and $1 ) {
187     push @where, "cust_main.agentnum = $1";
188   }
189   push @where, $FS::CurrentUser::CurrentUser->agentnums_sql(
190     'null_right' => 'View/link unlinked services',
191     'table' => 'cust_main'
192   );
193
194   #custnum
195   if ( $params->{'custnum'} =~ /^(\d+)$/ and $1 ) {
196     push @where, "custnum = $1";
197   }
198
199   #pkgpart, now properly untainted, can be arrayref
200   for my $pkgpart ( $params->{'pkgpart'} ) {
201     if ( ref $pkgpart ) {
202       my $where = join(',', map { /^(\d+)$/ ? $1 : () } @$pkgpart );
203       push @where, "cust_pkg.pkgpart IN ($where)" if $where;
204     }
205     elsif ( $pkgpart =~ /^(\d+)$/ ) {
206       push @where, "cust_pkg.pkgpart = $1";
207     }
208   }
209
210   #routernum, can be arrayref
211   for my $routernum ( $params->{'routernum'} ) {
212     push @from, 'LEFT JOIN addr_block USING ( blocknum )';
213     if ( ref $routernum and grep { $_ } @$routernum ) {
214       my $where = join(',', map { /^(\d+)$/ ? $1 : () } @$routernum );
215       push @where, "addr_block.routernum IN ($where)" if $where;
216     }
217     elsif ( $routernum =~ /^(\d+)$/ ) {
218       push @where, "addr_block.routernum = $1";
219     }
220   }
221
222   #sector and tower, as above
223   my @where_sector = $class->tower_sector_sql($params);
224   if ( @where_sector ) {
225     push @where, @where_sector;
226     push @from, 'LEFT JOIN tower_sector USING ( sectornum )';
227   }
228  
229   #svcnum
230   if ( $params->{'svcnum'} =~ /^(\d+)$/ ) {
231     push @where, "svcnum = $1";
232   }
233
234   #svcpart
235   if ( $params->{'svcpart'} =~ /^(\d+)$/ ) {
236     push @where, "svcpart = $1";
237   }
238
239   #ip_addr
240   if ( $params->{'ip_addr'} =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/ ) {
241     push @where, "ip_addr = '$1'";
242   }
243
244   #custnum
245   if ( $params->{'custnum'} =~ /^(\d+)$/ and $1) {
246     push @where, "custnum = $1";
247   }
248   
249   my $addl_from = join(' ', @from);
250   my $extra_sql = '';
251   $extra_sql = 'WHERE '.join(' AND ', @where) if @where;
252   my $count_query = "SELECT COUNT(*) FROM svc_broadband $addl_from $extra_sql";
253   return( {
254       'table'   => 'svc_broadband',
255       'hashref' => {},
256       'select'  => join(', ',
257         'svc_broadband.*',
258         'part_svc.svc',
259         'cust_main.custnum',
260         FS::UI::Web::cust_sql_fields($params->{'cust_fields'}),
261       ),
262       'extra_sql' => $extra_sql,
263       'addl_from' => $addl_from,
264       'order_by'  => "ORDER BY ".($params->{'order_by'} || 'svcnum'),
265       'count_query' => $count_query,
266     } );
267 }
268
269 =item search_sql STRING
270
271 Class method which returns an SQL fragment to search for the given string.
272
273 =cut
274
275 sub search_sql {
276   my( $class, $string ) = @_;
277   if ( $string =~ /^(\d{1,3}\.){3}\d{1,3}$/ ) {
278     $class->search_sql_field('ip_addr', $string );
279   }elsif ( $string =~ /^([a-fA-F0-9]{12})$/ ) {
280     $class->search_sql_field('mac_addr', uc($string));
281   }elsif ( $string =~ /^(([a-fA-F0-9]{1,2}:){5}([a-fA-F0-9]{1,2}))$/ ) {
282     $class->search_sql_field('mac_addr', uc("$2$3$4$5$6$7") );
283   } else {
284     '1 = 0'; #false
285   }
286 }
287
288 =item label
289
290 Returns the IP address.
291
292 =cut
293
294 sub label {
295   my $self = shift;
296   $self->ip_addr;
297 }
298
299 =item insert [ , OPTION => VALUE ... ]
300
301 Adds this record to the database.  If there is an error, returns the error,
302 otherwise returns false.
303
304 The additional fields pkgnum and svcpart (see FS::cust_svc) should be 
305 defined.  An FS::cust_svc record will be created and inserted.
306
307 Currently available options are: I<depend_jobnum>
308
309 If I<depend_jobnum> is set (to a scalar jobnum or an array reference of
310 jobnums), all provisioning jobs will have a dependancy on the supplied
311 jobnum(s) (they will not run until the specific job(s) complete(s)).
312
313 =cut
314
315 # Standard FS::svc_Common::insert
316
317 =item delete
318
319 Delete this record from the database.
320
321 =cut
322
323 # Standard FS::svc_Common::delete
324
325 =item replace OLD_RECORD
326
327 Replaces the OLD_RECORD with this one in the database.  If there is an error,
328 returns the error, otherwise returns false.
329
330 =cut
331
332 # Standard FS::svc_Common::replace
333
334 =item suspend
335
336 Called by the suspend method of FS::cust_pkg (see FS::cust_pkg).
337
338 =item unsuspend
339
340 Called by the unsuspend method of FS::cust_pkg (see FS::cust_pkg).
341
342 =item cancel
343
344 Called by the cancel method of FS::cust_pkg (see FS::cust_pkg).
345
346 =item check
347
348 Checks all fields to make sure this is a valid broadband service.  If there is
349 an error, returns the error, otherwise returns false.  Called by the insert
350 and replace methods.
351
352 =cut
353
354 sub check {
355   my $self = shift;
356   my $x = $self->setfixed;
357
358   return $x unless ref($x);
359
360   # remove delimiters
361   my $mac_addr = uc($self->get('mac_addr'));
362   $mac_addr =~ s/[-: ]//g;
363   $self->set('mac_addr', $mac_addr);
364
365   my $error =
366     $self->ut_numbern('svcnum')
367     || $self->ut_numbern('blocknum')
368     || $self->ut_foreign_keyn('sectornum', 'tower_sector', 'sectornum')
369     || $self->ut_textn('description')
370     || $self->ut_numbern('speed_up')
371     || $self->ut_numbern('speed_down')
372     || $self->ut_ipn('ip_addr')
373     || $self->ut_hexn('mac_addr')
374     || $self->ut_hexn('auth_key')
375     || $self->ut_coordn('latitude')
376     || $self->ut_coordn('longitude')
377     || $self->ut_sfloatn('altitude')
378     || $self->ut_textn('vlan_profile')
379     || $self->ut_textn('plan_id')
380   ;
381   return $error if $error;
382
383   if($self->speed_up < 0) { return 'speed_up must be positive'; }
384   if($self->speed_down < 0) { return 'speed_down must be positive'; }
385
386   my $cust_svc = $self->svcnum
387                  ? qsearchs('cust_svc', { 'svcnum' => $self->svcnum } )
388                  : '';
389   my $cust_pkg;
390   if ($cust_svc) {
391     $cust_pkg = $cust_svc->cust_pkg;
392   }else{
393     $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $self->pkgnum } );
394     return "Invalid pkgnum" unless $cust_pkg;
395   }
396     
397   if ($self->blocknum) {
398     $error = $self->ut_foreign_key('blocknum', 'addr_block', 'blocknum');
399     return $error if $error;
400   }
401
402   if ($cust_pkg && $self->blocknum) {
403     my $addr_agentnum = $self->addr_block->agentnum;
404     if ($addr_agentnum && $addr_agentnum != $cust_pkg->cust_main->agentnum) {
405       return "Address block does not service this customer";
406     }
407   }
408
409   if ( $cust_pkg && ! $self->latitude && ! $self->longitude ) {
410     my $l = $cust_pkg->cust_location_or_main;
411     if ( $l->ship_latitude && $l->ship_longitude ) {
412       $self->latitude(  $l->ship_latitude  );
413       $self->longitude( $l->ship_longitude );
414     } elsif ( $l->latitude && $l->longitude ) {
415       $self->latitude(  $l->latitude  );
416       $self->longitude( $l->longitude );
417     }
418   }
419
420   $error = $self->_check_ip_addr;
421   return $error if $error;
422
423   $self->SUPER::check;
424 }
425
426 sub _check_ip_addr {
427   my $self = shift;
428
429   if (not($self->ip_addr) or $self->ip_addr eq '0.0.0.0') {
430
431     return '' if $conf->exists('svc_broadband-allow_null_ip_addr'); #&& !$self->blocknum
432
433     return "Must supply either address or block"
434       unless $self->blocknum;
435     my $next_addr = $self->addr_block->next_free_addr;
436     if ($next_addr) {
437       $self->ip_addr($next_addr->addr);
438     } else {
439       return "No free addresses in addr_block (blocknum: ".$self->blocknum.")";
440     }
441
442   }
443
444   if (not($self->blocknum)) {
445     return "Must supply either address or block"
446       unless ($self->ip_addr and $self->ip_addr ne '0.0.0.0');
447     my @block = grep { $_->NetAddr->contains($self->NetAddr) }
448                  map { $_->addr_block }
449                  $self->allowed_routers;
450     if (scalar(@block)) {
451       $self->blocknum($block[0]->blocknum);
452     }else{
453       return "Address not with available block.";
454     }
455   }
456
457   # This should catch errors in the ip_addr.  If it doesn't,
458   # they'll almost certainly not map into the block anyway.
459   my $self_addr = $self->NetAddr; #netmask is /32
460   return ('Cannot parse address: ' . $self->ip_addr) unless $self_addr;
461
462   my $block_addr = $self->addr_block->NetAddr;
463   unless ($block_addr->contains($self_addr)) {
464     return 'blocknum '.$self->blocknum.' does not contain address '.$self->ip_addr;
465   }
466
467   my $router = $self->addr_block->router 
468     or return 'Cannot assign address from unallocated block:'.$self->addr_block->blocknum;
469   if(grep { $_->routernum == $router->routernum} $self->allowed_routers) {
470   } # do nothing
471   else {
472     return 'Router '.$router->routernum.' cannot provide svcpart '.$self->svcpart;
473   }
474
475   '';
476 }
477
478 sub _check_duplicate {
479   my $self = shift;
480
481   return "MAC already in use"
482     if ( $self->mac_addr &&
483          scalar( qsearch( 'svc_broadband', { 'mac_addr', $self->mac_addr } ) )
484        );
485
486   '';
487 }
488
489
490 =item NetAddr
491
492 Returns a NetAddr::IP object containing the IP address of this service.  The netmask 
493 is /32.
494
495 =cut
496
497 sub NetAddr {
498   my $self = shift;
499   new NetAddr::IP ($self->ip_addr);
500 }
501
502 =item addr_block
503
504 Returns the FS::addr_block record (i.e. the address block) for this broadband service.
505
506 =cut
507
508 sub addr_block {
509   my $self = shift;
510   qsearchs('addr_block', { blocknum => $self->blocknum });
511 }
512
513 =back
514
515 =item allowed_routers
516
517 Returns a list of allowed FS::router objects.
518
519 =cut
520
521 sub allowed_routers {
522   my $self = shift;
523   map { $_->router } qsearch('part_svc_router', { svcpart => $self->svcpart });
524 }
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 =head1 SEE ALSO
534
535 FS::svc_Common, FS::Record, FS::addr_block,
536 FS::part_svc, schema.html from the base documentation.
537
538 =cut
539
540 1;
541