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