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