refactor svc_acct and svc_broadband search into svc_Common, RT#21054
[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_svc {
179   my( $class, $params, $from, $where ) = @_;
180
181   #routernum, can be arrayref
182   for my $routernum ( $params->{'routernum'} ) {
183     # this no longer uses addr_block
184     if ( ref $routernum and grep { $_ } @$routernum ) {
185       my $in = join(',', map { /^(\d+)$/ ? $1 : () } @$routernum );
186       my @orwhere = ();
187       push @orwhere, "svc_broadband.routernum IN ($in)" if $in;
188       push @orwhere, "svc_broadband.routernum IS NULL" 
189         if grep /^none$/, @$routernum;
190       push @$where, '( '.join(' OR ', @orwhere).' )';
191     }
192     elsif ( $routernum =~ /^(\d+)$/ ) {
193       push @$where, "svc_broadband.routernum = $1";
194     }
195     elsif ( $routernum eq 'none' ) {
196       push @$where, "svc_broadband.routernum IS NULL";
197     }
198   }
199
200   #this should probably move to svc_Tower_Mixin, or maybe we never should have
201   # done svc_acct # towers (or, as mark thought, never should have done
202   # svc_broadband)
203
204   #sector and tower, as above
205   my @where_sector = $class->tower_sector_sql($params);
206   if ( @where_sector ) {
207     push @$where, @where_sector;
208     push @$from, 'LEFT JOIN tower_sector USING ( sectornum )';
209   }
210  
211   #ip_addr
212   if ( $params->{'ip_addr'} =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/ ) {
213     push @$where, "ip_addr = '$1'";
214   }
215
216 }
217
218 =item search_sql STRING
219
220 Class method which returns an SQL fragment to search for the given string.
221
222 =cut
223
224 sub search_sql {
225   my( $class, $string ) = @_;
226   if ( $string =~ /^(\d{1,3}\.){3}\d{1,3}$/ ) {
227     $class->search_sql_field('ip_addr', $string );
228   }elsif ( $string =~ /^([a-fA-F0-9]{12})$/ ) {
229     $class->search_sql_field('mac_addr', uc($string));
230   }elsif ( $string =~ /^(([a-fA-F0-9]{1,2}:){5}([a-fA-F0-9]{1,2}))$/ ) {
231     $class->search_sql_field('mac_addr', uc("$2$3$4$5$6$7") );
232   } else {
233     '1 = 0'; #false
234   }
235 }
236
237 =item label
238
239 Returns the IP address.
240
241 =cut
242
243 sub label {
244   my $self = shift;
245   my $label = 'IP:'. ($self->ip_addr || 'Unknown');
246   $label .= '", MAC:'. $self->mac_addr
247     if $self->mac_addr;
248   $label .= ' ('. $self->description. ')'
249     if $self->description;
250   return $label;
251 }
252
253 =item insert [ , OPTION => VALUE ... ]
254
255 Adds this record to the database.  If there is an error, returns the error,
256 otherwise returns false.
257
258 The additional fields pkgnum and svcpart (see FS::cust_svc) should be 
259 defined.  An FS::cust_svc record will be created and inserted.
260
261 Currently available options are: I<depend_jobnum>
262
263 If I<depend_jobnum> is set (to a scalar jobnum or an array reference of
264 jobnums), all provisioning jobs will have a dependancy on the supplied
265 jobnum(s) (they will not run until the specific job(s) complete(s)).
266
267 =cut
268
269 # Standard FS::svc_Common::insert
270
271 =item delete
272
273 Delete this record from the database.
274
275 =cut
276
277 # Standard FS::svc_Common::delete
278
279 =item replace OLD_RECORD
280
281 Replaces the OLD_RECORD with this one in the database.  If there is an error,
282 returns the error, otherwise returns false.
283
284 # Standard FS::svc_Common::replace
285
286 =item suspend
287
288 Called by the suspend method of FS::cust_pkg (see FS::cust_pkg).
289
290 =item unsuspend
291
292 Called by the unsuspend method of FS::cust_pkg (see FS::cust_pkg).
293
294 =item cancel
295
296 Called by the cancel method of FS::cust_pkg (see FS::cust_pkg).
297
298 =item check
299
300 Checks all fields to make sure this is a valid broadband service.  If there is
301 an error, returns the error, otherwise returns false.  Called by the insert
302 and replace methods.
303
304 =cut
305
306 sub check {
307   my $self = shift;
308   my $x = $self->setfixed;
309
310   return $x unless ref($x);
311
312   # remove delimiters
313   my $mac_addr = uc($self->get('mac_addr'));
314   $mac_addr =~ s/[-: ]//g;
315   $self->set('mac_addr', $mac_addr);
316
317   my $error =
318     $self->ut_numbern('svcnum')
319     || $self->ut_numbern('blocknum')
320     || $self->ut_foreign_keyn('routernum', 'router', 'routernum')
321     || $self->ut_foreign_keyn('sectornum', 'tower_sector', 'sectornum')
322     || $self->ut_textn('description')
323     || $self->ut_numbern('speed_up')
324     || $self->ut_numbern('speed_down')
325     || $self->ut_ipn('ip_addr')
326     || $self->ut_hexn('mac_addr')
327     || $self->ut_hexn('auth_key')
328     || $self->ut_coordn('latitude')
329     || $self->ut_coordn('longitude')
330     || $self->ut_sfloatn('altitude')
331     || $self->ut_textn('vlan_profile')
332     || $self->ut_textn('plan_id')
333   ;
334   return $error if $error;
335
336   if(($self->speed_up || 0) < 0) { return 'speed_up must be positive'; }
337   if(($self->speed_down || 0) < 0) { return 'speed_down must be positive'; }
338
339   my $cust_svc = $self->svcnum
340                  ? qsearchs('cust_svc', { 'svcnum' => $self->svcnum } )
341                  : '';
342   my $cust_pkg;
343   my $svcpart;
344   if ($cust_svc) {
345     $cust_pkg = $cust_svc->cust_pkg;
346     $svcpart = $cust_svc->svcpart;
347   }else{
348     $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $self->pkgnum } );
349     return "Invalid pkgnum" unless $cust_pkg;
350     $svcpart = $self->svcpart;
351   }
352   my $agentnum = $cust_pkg->cust_main->agentnum if $cust_pkg;
353
354   # assign IP address / router / block
355   $error = $self->svc_ip_check;
356   return $error if $error;
357   if ( !$self->ip_addr 
358        and !$conf->exists('svc_broadband-allow_null_ip_addr') ) {
359     return 'IP address is required';
360   }
361
362   if ( $cust_pkg && ! $self->latitude && ! $self->longitude ) {
363     my $l = $cust_pkg->cust_location_or_main;
364     if ( $l->ship_latitude && $l->ship_longitude ) {
365       $self->latitude(  $l->ship_latitude  );
366       $self->longitude( $l->ship_longitude );
367     } elsif ( $l->latitude && $l->longitude ) {
368       $self->latitude(  $l->latitude  );
369       $self->longitude( $l->longitude );
370     }
371   }
372
373   $self->SUPER::check;
374 }
375
376 sub _check_duplicate {
377   my $self = shift;
378   # Not a reliable check because the table isn't locked, but 
379   # that's why we have a unique index.  This is just to give a
380   # friendlier error message.
381   my @dup;
382   @dup = $self->find_duplicates('global', 'mac_addr');
383   if ( @dup ) {
384     return "MAC address in use (svcnum ".$dup[0]->svcnum.")";
385   }
386
387   '';
388 }
389
390 =item mac_addr_formatted CASE DELIMITER
391
392 Format the MAC address (for use by exports).  If CASE starts with "l"
393 (for "lowercase"), it's returned in lowercase.  DELIMITER is inserted
394 between octets.
395
396 =cut
397
398 sub mac_addr_formatted {
399   my $self = shift;
400   my ($case, $delim) = @_;
401   my $addr = $self->mac_addr;
402   $addr = lc($addr) if $case =~ /^l/i;
403   join( $delim || '', $addr =~ /../g );
404 }
405
406 #class method
407 sub _upgrade_data {
408   my $class = shift;
409
410   local($FS::svc_Common::noexport_hack) = 1;
411
412   # set routernum to addr_block.routernum
413   foreach my $self (qsearch('svc_broadband', {
414       blocknum => {op => '!=', value => ''},
415       routernum => ''
416     })) {
417     my $addr_block = $self->addr_block;
418     if ( !$addr_block ) {
419       # super paranoid mode
420       warn "WARNING: svcnum ".$self->svcnum." is assigned to addr_block ".$self->blocknum.", which does not exist; skipped.\n";
421       next;
422     }
423     my $ip_addr = $self->ip_addr;
424     my $routernum = $addr_block->routernum;
425     if ( $routernum ) {
426       $self->set(routernum => $routernum);
427       my $error = $self->check;
428       # sanity check: don't allow this to change IP address or block
429       # (other than setting blocknum to null for a non-auto-assigned router)
430       if ( $self->ip_addr ne $ip_addr 
431         or ($self->blocknum and $self->blocknum != $addr_block->blocknum)) {
432         warn "WARNING: Upgrading service ".$self->svcnum." would change its block/address; skipped.\n";
433         next;
434       }
435
436       $error ||= $self->replace;
437       warn "WARNING: error assigning routernum $routernum to service ".$self->svcnum.
438           ":\n$error; skipped\n"
439         if $error;
440     }
441     else {
442       warn "svcnum ".$self->svcnum.
443         ": no routernum in address block ".$addr_block->cidr.", skipped\n";
444     }
445   }
446
447   # assign blocknums to services that should have them
448   my @all_blocks = qsearch('addr_block', { });
449   SVC: foreach my $self ( 
450     qsearch({
451         'select' => 'svc_broadband.*',
452         'table' => 'svc_broadband',
453         'addl_from' => 'JOIN router USING (routernum)',
454         'hashref' => {},
455         'extra_sql' => 'WHERE svc_broadband.blocknum IS NULL '.
456                        'AND router.manual_addr IS NULL',
457     }) 
458   ) {
459    
460     next SVC if $self->ip_addr eq '';
461     my $NetAddr = $self->NetAddr;
462     # inefficient, but should only need to run once
463     foreach my $block (@all_blocks) {
464       if ($block->NetAddr->contains($NetAddr)) {
465         $self->set(blocknum => $block->blocknum);
466         my $error = $self->replace;
467         warn "WARNING: error assigning blocknum ".$block->blocknum.
468         " to service ".$self->svcnum."\n$error; skipped\n"
469           if $error;
470         next SVC;
471       }
472     }
473     warn "WARNING: no block found containing ".$NetAddr->addr." for service ".
474       $self->svcnum;
475     #next SVC;
476   }
477
478   '';
479 }
480
481 =back
482
483 =head1 BUGS
484
485 The business with sb_field has been 'fixed', in a manner of speaking.
486
487 allowed_routers isn't agent virtualized because part_svc isn't agent
488 virtualized
489
490 Having both routernum and blocknum as foreign keys is somewhat dubious.
491
492 =head1 SEE ALSO
493
494 FS::svc_Common, FS::Record, FS::addr_block,
495 FS::part_svc, schema.html from the base documentation.
496
497 =cut
498
499 1;
500