RT# 82137 - added processing fee template substitution.
[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   my $conf = new FS::Conf;
100   my $ip_addr_required = $conf->exists('svc_broadband-allow_null_ip_addr') ? '' : '1';
101   {
102     'name' => 'Wireless broadband',
103     'name_plural' => 'Wireless broadband services',
104     'longname_plural' => 'Fixed wireless broadband services',
105     'display_weight' => 50,
106     'cancel_weight'  => 70,
107     'ip_field' => 'ip_addr',
108     'manual_require' => 1,
109     'fields' => {
110       'svcnum'      => 'Service',
111       'description' => 'Descriptive label',
112       'speed_up'    => {
113                          'label'    => 'Upload speed (Kbps)',
114                          'type'     => 'fcc_477_speed',
115                          'def_info' => 'both upload and download speed must be set to FCC 477 information if using that modifier',
116                        },
117       'speed_down'  => {
118                          'label'    => 'Download speed (Kbps)',
119                          'type'     => 'fcc_477_speed',
120                          'def_info' => 'both upload and download speed must be set to FCC 477 information if using that modifier',
121                        },
122       'ip_addr'     => {
123                          'label' => 'IP address',
124                          'required' => $ip_addr_required,
125                        },
126       'blocknum'    => {
127                          'label'             => 'Address block',
128                          'type'              => 'select',
129                          'select_table'      => 'addr_block',
130                           'select_key'       => 'blocknum',
131                          'select_label'      => 'cidr',
132                          'disable_inventory' => 1,
133                        },
134      'plan_id' => 'Service Plan Id',
135      'performance_profile' => 'Peformance Profile',
136      'authkey'      => 'Authentication key',
137      'mac_addr'     => 'MAC address',
138      'latitude'     => 'Latitude',
139      'longitude'    => 'Longitude',
140      'altitude'     => 'Altitude',
141      'vlan_profile' => 'VLAN profile',
142      'sectornum'    => 'Tower/sector',
143      'routernum'    => 'Router/block',
144      'usergroup'    => { 
145                          label => 'RADIUS groups',
146                          type  => 'select-radius_group.html',
147                          #select_table => 'radius_group',
148                          #select_key   => 'groupnum',
149                          #select_label => 'groupname',
150                          disable_select => 1,
151                          disable_inventory => 1,
152                          multiple => 1,
153                        },
154       'radio_serialnum' => 'Radio Serial Number',
155       'radio_location'  => 'Radio Location',
156       'poe_location'    => 'POE Location',
157       'rssi'            => 'RSSI',
158       'suid'            => 'SUID',
159       'shared_svcnum'   => { label             => 'Shared Service',
160                              type              => 'search-svc_broadband',
161                              disable_inventory => 1,
162                            },
163       'serviceid' => 'Torrus serviceid', #but is should be hidden
164       'speed_test_up'      => { 'label' => 'Speed test upload (Kbps)' },
165       'speed_test_down'    => { 'label' => 'Speed test download (Kbps)' },
166       'speed_test_latency' => 'Speed test latency (ms)',
167     },
168   };
169 }
170
171 sub table { 'svc_broadband'; }
172
173 sub table_dupcheck_fields { ( 'ip_addr', 'mac_addr' ); }
174
175 =item search HASHREF
176
177 Class method which returns a qsearch hash expression to search for parameters
178 specified in HASHREF.
179
180 Parameters:
181
182 =over 4
183
184 =item unlinked - set to search for all unlinked services.  Overrides all other options.
185
186 =item agentnum
187
188 =item custnum
189
190 =item svcpart
191
192 =item ip_addr
193
194 =item pkgpart - arrayref
195
196 =item routernum - arrayref
197
198 =item sectornum - arrayref
199
200 =item towernum - arrayref
201
202 =item order_by
203
204 =back
205
206 =cut
207
208 sub _search_svc {
209   my( $class, $params, $from, $where ) = @_;
210
211   #routernum, can be arrayref
212   for my $routernum ( $params->{'routernum'} ) {
213     # this no longer uses addr_block
214     if ( ref $routernum and grep { $_ } @$routernum ) {
215       my $in = join(',', map { /^(\d+)$/ ? $1 : () } @$routernum );
216       my @orwhere = ();
217       push @orwhere, "svc_broadband.routernum IN ($in)" if $in;
218       push @orwhere, "svc_broadband.routernum IS NULL" 
219         if grep /^none$/, @$routernum;
220       push @$where, '( '.join(' OR ', @orwhere).' )';
221     }
222     elsif ( $routernum =~ /^(\d+)$/ ) {
223       push @$where, "svc_broadband.routernum = $1";
224     }
225     elsif ( $routernum eq 'none' ) {
226       push @$where, "svc_broadband.routernum IS NULL";
227     }
228   }
229
230   #this should probably move to svc_Tower_Mixin, or maybe we never should have
231   # done svc_acct # towers (or, as mark thought, never should have done
232   # svc_broadband)
233
234   #sector and tower, as above
235   my @where_sector = $class->tower_sector_sql($params);
236   if ( @where_sector ) {
237     push @$where, @where_sector;
238     push @$from, 'LEFT JOIN tower_sector USING ( sectornum )';
239   }
240  
241   #ip_addr
242   if ( $params->{'ip_addr'} =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/ ) {
243     push @$where, "ip_addr = '$1'";
244   }
245
246 }
247
248 =item search_sql STRING
249
250 Class method which returns an SQL fragment to search for the given string.
251
252 =cut
253
254 sub search_sql {
255   my( $class, $string ) = @_;
256   if ( $string =~ /^(\d{1,3}\.){3}\d{1,3}$/ ) {
257     $class->search_sql_field('ip_addr', $string );
258   } elsif ( $string =~ /^([A-F0-9]{12})$/i ) {
259     $class->search_sql_field('mac_addr', uc($string));
260   } elsif ( $string =~ /^(([A-F0-9]{2}:){5}([A-F0-9]{2}))$/i ) {
261     $string =~ s/://g;
262     $class->search_sql_field('mac_addr', uc($string) );
263   } elsif ( $string =~ /^(\d+)$/ ) {
264     my $table = $class->table;
265     "$table.svcnum = $1";
266   } else {
267     '1 = 0'; #false
268   }
269 }
270
271 =item smart_search STRING
272
273 =cut
274
275 sub smart_search {
276   my( $class, $string ) = @_;
277   qsearch({
278     'table'     => $class->table, #'svc_broadband',
279     'hashref'   => {},
280     'extra_sql' => 'WHERE '. $class->search_sql($string),
281   });
282 }
283
284 =item label
285
286 Returns the IP address, MAC address and description.
287
288 =cut
289
290 sub label {
291   my $self = shift;
292   my $label = 'IP:'. ($self->ip_addr || 'Unknown');
293   $label .= ', MAC:'. $self->mac_addr
294     if $self->mac_addr;
295   $label .= ' ('. $self->description. ')'
296     if $self->description;
297   return $label;
298 }
299
300 =item insert [ , OPTION => VALUE ... ]
301
302 Adds this record to the database.  If there is an error, returns the error,
303 otherwise returns false.
304
305 The additional fields pkgnum and svcpart (see FS::cust_svc) should be 
306 defined.  An FS::cust_svc record will be created and inserted.
307
308 Currently available options are: I<depend_jobnum>
309
310 If I<depend_jobnum> is set (to a scalar jobnum or an array reference of
311 jobnums), all provisioning jobs will have a dependancy on the supplied
312 jobnum(s) (they will not run until the specific job(s) complete(s)).
313
314 =cut
315
316 # Standard FS::svc_Common::insert
317
318 =item delete
319
320 Delete this record from the database.
321
322 =cut
323
324 # Standard FS::svc_Common::delete
325
326 =item replace OLD_RECORD
327
328 Replaces the OLD_RECORD with this one in the database.  If there is an error,
329 returns the error, otherwise returns false.
330
331 # Standard FS::svc_Common::replace
332
333 =item suspend
334
335 Called by the suspend method of FS::cust_pkg (see FS::cust_pkg).
336
337 =item unsuspend
338
339 Called by the unsuspend method of FS::cust_pkg (see FS::cust_pkg).
340
341 =item cancel
342
343 Called by the cancel method of FS::cust_pkg (see FS::cust_pkg).
344
345 =item check
346
347 Checks all fields to make sure this is a valid broadband service.  If there is
348 an error, returns the error, otherwise returns false.  Called by the insert
349 and replace methods.
350
351 =cut
352
353 sub check {
354   my $self = shift;
355   my $x = $self->setfixed;
356
357   return $x unless ref($x);
358
359   # remove delimiters
360   my $mac_addr = uc($self->get('mac_addr'));
361   $mac_addr =~ s/[\W_]//g;
362   $self->set('mac_addr', $mac_addr);
363
364   my $error =
365     $self->ut_numbern('svcnum')
366     || $self->ut_numbern('blocknum')
367     || $self->ut_foreign_keyn('routernum', 'router', 'routernum')
368     || $self->ut_foreign_keyn('sectornum', 'tower_sector', 'sectornum')
369     || $self->ut_textn('description')
370     || $self->ut_numbern('speed_up')
371     || $self->ut_numbern('speed_down')
372     || $self->ut_numbern('speed_test_up')
373     || $self->ut_numbern('speed_test_down')
374     || $self->ut_ipn('ip_addr')
375     || $self->ut_hexn('mac_addr')
376     || $self->ut_hexn('auth_key')
377     || $self->ut_coordn('latitude')
378     || $self->ut_coordn('longitude')
379     || $self->ut_sfloatn('altitude')
380     || $self->ut_textn('vlan_profile')
381     || $self->ut_textn('plan_id')
382     || $self->ut_alphan('radio_serialnum')
383     || $self->ut_textn('radio_location')
384     || $self->ut_textn('poe_location')
385     || $self->ut_snumbern('rssi')
386     || $self->ut_numbern('suid')
387     || $self->ut_foreign_keyn('shared_svcnum', 'svc_broadband', 'svcnum')
388     || $self->ut_textn('serviceid') #too lenient?
389   ;
390   return $error if $error;
391
392   if(($self->speed_up || 0) < 0) { return 'speed_up must be positive'; }
393   if(($self->speed_down || 0) < 0) { return 'speed_down must be positive'; }
394
395   my $cust_svc = $self->svcnum
396                  ? qsearchs('cust_svc', { 'svcnum' => $self->svcnum } )
397                  : '';
398   my $cust_pkg;
399   my $svcpart;
400   if ($cust_svc) {
401     $cust_pkg = $cust_svc->cust_pkg;
402     $svcpart = $cust_svc->svcpart;
403   }else{
404     $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $self->pkgnum } );
405     return "Invalid pkgnum" unless $cust_pkg;
406     $svcpart = $self->svcpart;
407   }
408   my $agentnum = $cust_pkg->cust_main->agentnum if $cust_pkg;
409
410   # assign IP address / router / block
411   $error = $self->svc_ip_check;
412   return $error if $error;
413   if ( !$self->ip_addr 
414        and !$conf->exists('svc_broadband-allow_null_ip_addr') ) {
415     return 'IP address is required';
416   }
417
418   if ( $cust_pkg && ! $self->latitude && ! $self->longitude ) {
419     my $l = $cust_pkg->cust_location_or_main;
420     if ( $l->ship_latitude && $l->ship_longitude ) {
421       $self->latitude(  $l->ship_latitude  );
422       $self->longitude( $l->ship_longitude );
423     } elsif ( $l->latitude && $l->longitude ) {
424       $self->latitude(  $l->latitude  );
425       $self->longitude( $l->longitude );
426     }
427   }
428
429   $self->SUPER::check;
430 }
431
432 sub _check_duplicate {
433   my $self = shift;
434   # Not a reliable check because the table isn't locked, but 
435   # that's why we have a unique index.  This is just to give a
436   # friendlier error message.
437   my @dup;
438   @dup = $self->find_duplicates('global', 'mac_addr');
439   if ( @dup ) {
440     return "MAC address in use (svcnum ".$dup[0]->svcnum.")";
441   }
442
443   '';
444 }
445
446 #class method
447 sub _upgrade_data {
448   my $class = shift;
449
450   local($FS::svc_Common::noexport_hack) = 1;
451
452   # fix wrong-case MAC addresses
453   my $dbh = dbh;
454   $dbh->do('UPDATE svc_broadband SET mac_addr = UPPER(mac_addr);')
455     or die $dbh->errstr;
456
457   # set routernum to addr_block.routernum
458   foreach my $self (qsearch('svc_broadband', {
459       blocknum => {op => '!=', value => ''},
460       routernum => ''
461     })) {
462     my $addr_block = $self->addr_block;
463     if ( !$addr_block ) {
464       # super paranoid mode
465       warn "WARNING: svcnum ".$self->svcnum." is assigned to addr_block ".$self->blocknum.", which does not exist; skipped.\n";
466       next;
467     }
468     my $ip_addr = $self->ip_addr;
469     my $routernum = $addr_block->routernum;
470     if ( $routernum ) {
471       $self->set(routernum => $routernum);
472       my $error = $self->check;
473       # sanity check: don't allow this to change IP address or block
474       # (other than setting blocknum to null for a non-auto-assigned router)
475       if ( $self->ip_addr ne $ip_addr 
476         or ($self->blocknum and $self->blocknum != $addr_block->blocknum)) {
477         warn "WARNING: Upgrading service ".$self->svcnum." would change its block/address; skipped.\n";
478         next;
479       }
480
481       $error ||= $self->replace;
482       warn "WARNING: error assigning routernum $routernum to service ".$self->svcnum.
483           ":\n$error; skipped\n"
484         if $error;
485     }
486     else {
487       warn "svcnum ".$self->svcnum.
488         ": no routernum in address block ".$addr_block->cidr.", skipped\n";
489     }
490   }
491
492   # assign blocknums to services that should have them
493   my @all_blocks = qsearch('addr_block', { });
494   SVC: foreach my $self ( 
495     qsearch({
496         'select' => 'svc_broadband.*',
497         'table' => 'svc_broadband',
498         'addl_from' => 'JOIN router USING (routernum)',
499         'hashref' => {},
500         'extra_sql' => 'WHERE svc_broadband.blocknum IS NULL '.
501                        'AND router.manual_addr IS NULL',
502     }) 
503   ) {
504    
505     next SVC if $self->ip_addr eq '';
506     my $NetAddr = $self->NetAddr;
507     # inefficient, but should only need to run once
508     foreach my $block (@all_blocks) {
509       if ($block->NetAddr->contains($NetAddr)) {
510         $self->set(blocknum => $block->blocknum);
511         my $error = $self->replace;
512         warn "WARNING: error assigning blocknum ".$block->blocknum.
513         " to service ".$self->svcnum."\n$error; skipped\n"
514           if $error;
515         next SVC;
516       }
517     }
518     warn "WARNING: no block found containing ".$NetAddr->addr." for service ".
519       $self->svcnum;
520     #next SVC;
521   }
522
523   require FS::Misc::FixIPFormat;
524   FS::Misc::FixIPFormat::fix_bad_addresses_in_table(
525       'svc_broadband', 'svcnum', 'ip_addr',
526   );
527
528   '';
529 }
530
531 =back
532
533 =head1 BUGS
534
535 The business with sb_field has been 'fixed', in a manner of speaking.
536
537 allowed_routers isn't agent virtualized because part_svc isn't agent
538 virtualized
539
540 Having both routernum and blocknum as foreign keys is somewhat dubious.
541
542 =head1 SEE ALSO
543
544 FS::svc_Common, FS::Record, FS::addr_block,
545 FS::part_svc, schema.html from the base documentation.
546
547 =cut
548
549 1;