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