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