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