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