broadband_sql export, #15924
[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 { no warnings 'redefine'; 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'    => 
107       #{ '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      'sectornum'    => 'Tower/sector',
123      'routernum'    => 'Router/block',
124      'usergroup'    => { 
125                          label => 'RADIUS groups',
126                          type  => 'select-radius_group.html',
127                          #select_table => 'radius_group',
128                          #select_key   => 'groupnum',
129                          #select_label => 'groupname',
130                          disable_inventory => 1,
131                          multiple => 1,
132                        },
133     },
134   };
135 }
136
137 sub table { 'svc_broadband'; }
138
139 sub table_dupcheck_fields { ( 'mac_addr' ); }
140
141 =item search HASHREF
142
143 Class method which returns a qsearch hash expression to search for parameters
144 specified in HASHREF.
145
146 Parameters:
147
148 =over 4
149
150 =item unlinked - set to search for all unlinked services.  Overrides all other options.
151
152 =item agentnum
153
154 =item custnum
155
156 =item svcpart
157
158 =item ip_addr
159
160 =item pkgpart - arrayref
161
162 =item routernum - arrayref
163
164 =item sectornum - arrayref
165
166 =item towernum - arrayref
167
168 =item order_by
169
170 =back
171
172 =cut
173
174 sub search {
175   my ($class, $params) = @_;
176   my @where = ();
177   my @from = (
178     'LEFT JOIN cust_svc  USING ( svcnum  )',
179     'LEFT JOIN part_svc  USING ( svcpart )',
180     'LEFT JOIN cust_pkg  USING ( pkgnum  )',
181     'LEFT JOIN cust_main USING ( custnum )',
182   );
183
184   # based on FS::svc_acct::search, probably the most mature of the bunch
185   #unlinked
186   push @where, 'pkgnum IS NULL' if $params->{'unlinked'};
187   
188   #agentnum
189   if ( $params->{'agentnum'} =~ /^(\d+)$/ and $1 ) {
190     push @where, "cust_main.agentnum = $1";
191   }
192   push @where, $FS::CurrentUser::CurrentUser->agentnums_sql(
193     'null_right' => 'View/link unlinked services',
194     'table' => 'cust_main'
195   );
196
197   #custnum
198   if ( $params->{'custnum'} =~ /^(\d+)$/ and $1 ) {
199     push @where, "custnum = $1";
200   }
201
202   #pkgpart, now properly untainted, can be arrayref
203   for my $pkgpart ( $params->{'pkgpart'} ) {
204     if ( ref $pkgpart ) {
205       my $where = join(',', map { /^(\d+)$/ ? $1 : () } @$pkgpart );
206       push @where, "cust_pkg.pkgpart IN ($where)" if $where;
207     }
208     elsif ( $pkgpart =~ /^(\d+)$/ ) {
209       push @where, "cust_pkg.pkgpart = $1";
210     }
211   }
212
213   #routernum, can be arrayref
214   for my $routernum ( $params->{'routernum'} ) {
215     # this no longer uses addr_block
216     if ( ref $routernum and grep { $_ } @$routernum ) {
217       my $in = join(',', map { /^(\d+)$/ ? $1 : () } @$routernum );
218       my @orwhere;
219       push @orwhere, "svc_broadband.routernum IN ($in)" if $in;
220       push @orwhere, "svc_broadband.routernum IS NULL" 
221         if grep /^none$/, @$routernum;
222       push @where, '( '.join(' OR ', @orwhere).' )';
223     }
224     elsif ( $routernum =~ /^(\d+)$/ ) {
225       push @where, "svc_broadband.routernum = $1";
226     }
227     elsif ( $routernum eq 'none' ) {
228       push @where, "svc_broadband.routernum IS NULL";
229     }
230   }
231
232   #sector and tower, as above
233   my @where_sector = $class->tower_sector_sql($params);
234   if ( @where_sector ) {
235     push @where, @where_sector;
236     push @from, 'LEFT JOIN tower_sector USING ( sectornum )';
237   }
238  
239   #svcnum
240   if ( $params->{'svcnum'} =~ /^(\d+)$/ ) {
241     push @where, "svcnum = $1";
242   }
243
244   #svcpart
245   if ( $params->{'svcpart'} =~ /^(\d+)$/ ) {
246     push @where, "svcpart = $1";
247   }
248
249   #ip_addr
250   if ( $params->{'ip_addr'} =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/ ) {
251     push @where, "ip_addr = '$1'";
252   }
253
254   #custnum
255   if ( $params->{'custnum'} =~ /^(\d+)$/ and $1) {
256     push @where, "custnum = $1";
257   }
258   
259   my $addl_from = join(' ', @from);
260   my $extra_sql = '';
261   $extra_sql = 'WHERE '.join(' AND ', @where) if @where;
262   my $count_query = "SELECT COUNT(*) FROM svc_broadband $addl_from $extra_sql";
263   return( {
264       'table'   => 'svc_broadband',
265       'hashref' => {},
266       'select'  => join(', ',
267         'svc_broadband.*',
268         'part_svc.svc',
269         'cust_main.custnum',
270         FS::UI::Web::cust_sql_fields($params->{'cust_fields'}),
271       ),
272       'extra_sql' => $extra_sql,
273       'addl_from' => $addl_from,
274       'order_by'  => "ORDER BY ".($params->{'order_by'} || 'svcnum'),
275       'count_query' => $count_query,
276     } );
277 }
278
279 =item search_sql STRING
280
281 Class method which returns an SQL fragment to search for the given string.
282
283 =cut
284
285 sub search_sql {
286   my( $class, $string ) = @_;
287   if ( $string =~ /^(\d{1,3}\.){3}\d{1,3}$/ ) {
288     $class->search_sql_field('ip_addr', $string );
289   }elsif ( $string =~ /^([a-fA-F0-9]{12})$/ ) {
290     $class->search_sql_field('mac_addr', uc($string));
291   }elsif ( $string =~ /^(([a-fA-F0-9]{1,2}:){5}([a-fA-F0-9]{1,2}))$/ ) {
292     $class->search_sql_field('mac_addr', uc("$2$3$4$5$6$7") );
293   } else {
294     '1 = 0'; #false
295   }
296 }
297
298 =item label
299
300 Returns the IP address.
301
302 =cut
303
304 sub label {
305   my $self = shift;
306   $self->ip_addr;
307 }
308
309 =item insert [ , OPTION => VALUE ... ]
310
311 Adds this record to the database.  If there is an error, returns the error,
312 otherwise returns false.
313
314 The additional fields pkgnum and svcpart (see FS::cust_svc) should be 
315 defined.  An FS::cust_svc record will be created and inserted.
316
317 Currently available options are: I<depend_jobnum>
318
319 If I<depend_jobnum> is set (to a scalar jobnum or an array reference of
320 jobnums), all provisioning jobs will have a dependancy on the supplied
321 jobnum(s) (they will not run until the specific job(s) complete(s)).
322
323 # Standard FS::svc_Common::insert
324
325 =item delete
326
327 Delete this record from the database.
328
329 =cut
330
331 # Standard FS::svc_Common::delete
332
333 =item replace OLD_RECORD
334
335 Replaces the OLD_RECORD with this one in the database.  If there is an error,
336 returns the error, otherwise returns false.
337
338 # Standard FS::svc_Common::replace
339
340 =item suspend
341
342 Called by the suspend method of FS::cust_pkg (see FS::cust_pkg).
343
344 =item unsuspend
345
346 Called by the unsuspend method of FS::cust_pkg (see FS::cust_pkg).
347
348 =item cancel
349
350 Called by the cancel method of FS::cust_pkg (see FS::cust_pkg).
351
352 =item check
353
354 Checks all fields to make sure this is a valid broadband service.  If there is
355 an error, returns the error, otherwise returns false.  Called by the insert
356 and replace methods.
357
358 =cut
359
360 sub check {
361   my $self = shift;
362   my $x = $self->setfixed;
363
364   return $x unless ref($x);
365
366   # remove delimiters
367   my $mac_addr = uc($self->get('mac_addr'));
368   $mac_addr =~ s/[-: ]//g;
369   $self->set('mac_addr', $mac_addr);
370
371   my $error =
372     $self->ut_numbern('svcnum')
373     || $self->ut_numbern('blocknum')
374     || $self->ut_foreign_keyn('routernum', 'router', 'routernum')
375     || $self->ut_foreign_keyn('sectornum', 'tower_sector', 'sectornum')
376     || $self->ut_textn('description')
377     || $self->ut_numbern('speed_up')
378     || $self->ut_numbern('speed_down')
379     || $self->ut_ipn('ip_addr')
380     || $self->ut_hexn('mac_addr')
381     || $self->ut_hexn('auth_key')
382     || $self->ut_coordn('latitude')
383     || $self->ut_coordn('longitude')
384     || $self->ut_sfloatn('altitude')
385     || $self->ut_textn('vlan_profile')
386     || $self->ut_textn('plan_id')
387   ;
388   return $error if $error;
389
390   if(($self->speed_up || 0) < 0) { return 'speed_up must be positive'; }
391   if(($self->speed_down || 0) < 0) { return 'speed_down must be positive'; }
392
393   my $cust_svc = $self->svcnum
394                  ? qsearchs('cust_svc', { 'svcnum' => $self->svcnum } )
395                  : '';
396   my $cust_pkg;
397   my $svcpart;
398   if ($cust_svc) {
399     $cust_pkg = $cust_svc->cust_pkg;
400     $svcpart = $cust_svc->svcpart;
401   }else{
402     $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $self->pkgnum } );
403     return "Invalid pkgnum" unless $cust_pkg;
404     $svcpart = $self->svcpart;
405   }
406   my $agentnum = $cust_pkg->cust_main->agentnum if $cust_pkg;
407
408   if ($self->routernum) {
409     return "Router ".$self->routernum." does not provide this service"
410       unless qsearchs('part_svc_router', { 
411         svcpart => $svcpart,
412         routernum => $self->routernum
413     });
414   
415     my $router = $self->router;
416     return "Router ".$self->routernum." does not serve this customer"
417       if $router->agentnum and $router->agentnum != $agentnum;
418
419     if ( $router->auto_addr ) {
420       my $error = $self->assign_ip_addr;
421       return $error if $error;
422     }
423     else {
424       $self->blocknum('');
425     }
426   } # if $self->routernum
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   $error = $self->_check_ip_addr;
440   return $error if $error;
441
442   $self->SUPER::check;
443 }
444
445 =item assign_ip_addr
446
447 Assign an address block matching the selected router, and the selected block
448 if there is one.
449
450 =cut
451
452 sub assign_ip_addr {
453   my $self = shift;
454   my @blocks;
455   my $ip_addr;
456
457   if ( $self->blocknum and $self->addr_block->routernum == $self->routernum ) {
458     # simple case: user chose a block, find an address in that block
459     # (this overrides an existing IP address if it's not in the block)
460     @blocks = ($self->addr_block);
461   }
462   elsif ( $self->routernum ) {
463     @blocks = $self->router->auto_addr_block;
464   }
465   else { 
466     return '';
467   }
468
469   foreach my $block ( @blocks ) {
470     if ( $self->ip_addr and $block->NetAddr->contains($self->NetAddr) ) {
471       # don't change anything
472       return '';
473     }
474     $ip_addr = $block->next_free_addr;
475     last if $ip_addr;
476   }
477   if ( $ip_addr ) {
478     $self->set(ip_addr => $ip_addr->addr);
479     return '';
480   }
481   else {
482     return 'No IP address available on this router';
483   }
484 }
485
486 sub _check_ip_addr {
487   my $self = shift;
488
489   if (not($self->ip_addr) or $self->ip_addr eq '0.0.0.0') {
490     return '' if $conf->exists('svc_broadband-allow_null_ip_addr'); 
491     return 'IP address required';
492   }
493 #  if (my $dup = qsearchs('svc_broadband', {
494 #        ip_addr => $self->ip_addr,
495 #        svcnum  => {op=>'!=', value => $self->svcnum}
496 #      }) ) {
497 #    return 'IP address conflicts with svcnum '.$dup->svcnum;
498 #  }
499   '';
500 }
501
502 sub _check_duplicate {
503   my $self = shift;
504
505   return "MAC already in use"
506     if ( $self->mac_addr &&
507          scalar( qsearch( 'svc_broadband', { 'mac_addr', $self->mac_addr } ) )
508        );
509
510   '';
511 }
512
513
514 =item NetAddr
515
516 Returns a NetAddr::IP object containing the IP address of this service.  The netmask 
517 is /32.
518
519 =cut
520
521 sub NetAddr {
522   my $self = shift;
523   new NetAddr::IP ($self->ip_addr);
524 }
525
526 =item addr_block
527
528 Returns the FS::addr_block record (i.e. the address block) for this broadband service.
529
530 =cut
531
532 sub addr_block {
533   my $self = shift;
534   qsearchs('addr_block', { blocknum => $self->blocknum });
535 }
536
537 =item router
538
539 Returns the FS::router record for this service.
540
541 =cut
542
543 sub router {
544   my $self = shift;
545   qsearchs('router', { routernum => $self->routernum });
546 }
547
548 =item allowed_routers
549
550 Returns a list of allowed FS::router objects.
551
552 =cut
553
554 sub allowed_routers {
555   my $self = shift;
556   my $svcpart = $self->svcnum ? $self->cust_svc->svcpart : $self->svcpart;
557   map { $_->router } qsearch('part_svc_router', 
558     { svcpart => $self->cust_svc->svcpart });
559 }
560
561 =back
562
563
564 =item mac_addr_formatted CASE DELIMITER
565
566 Format the MAC address (for use by exports).  If CASE starts with "l"
567 (for "lowercase"), it's returned in lowercase.  DELIMITER is inserted
568 between octets.
569
570 =cut
571
572 sub mac_addr_formatted {
573   my $self = shift;
574   my ($case, $delim) = @_;
575   my $addr = $self->mac_addr;
576   $addr = lc($addr) if $case =~ /^l/i;
577   join( $delim || '', $addr =~ /../g );
578 }
579
580 =back
581
582
583 #class method
584 sub _upgrade_data {
585   my $class = shift;
586
587   # set routernum to addr_block.routernum
588   foreach my $self (qsearch('svc_broadband', {
589       blocknum => {op => '!=', value => ''},
590       routernum => ''
591     })) {
592     my $addr_block = $self->addr_block;
593     if ( my $routernum = $addr_block->routernum ) {
594       $self->set(routernum => $routernum);
595       my $error = $self->replace;
596       die "error assigning routernum $routernum to service ".$self->svcnum.
597           ":\n$error\n"
598         if $error;
599     }
600     else {
601       warn "svcnum ".$self->svcnum.
602         ": no routernum in address block ".$addr_block->cidr.", skipped\n";
603     }
604   }
605   '';
606 }
607
608 =head1 BUGS
609
610 The business with sb_field has been 'fixed', in a manner of speaking.
611
612 allowed_routers isn't agent virtualized because part_svc isn't agent
613 virtualized
614
615 Having both routernum and blocknum as foreign keys is somewhat dubious.
616
617 =head1 SEE ALSO
618
619 FS::svc_Common, FS::Record, FS::addr_block,
620 FS::part_svc, schema.html from the base documentation.
621
622 =cut
623
624 1;
625