eWay self-signup fixes
[freeside.git] / FS / FS / svc_broadband.pm
1 package FS::svc_broadband;
2
3 use strict;
4 use vars qw(@ISA $conf);
5 use FS::Record qw( qsearchs qsearch dbh );
6 use FS::svc_Common;
7 use FS::cust_svc;
8 use FS::addr_block;
9 use FS::part_svc_router;
10 use NetAddr::IP;
11
12 @ISA = qw( FS::svc_Common );
13
14 $FS::UID::callback{'FS::svc_broadband'} = sub { 
15   $conf = new FS::Conf;
16 };
17
18 =head1 NAME
19
20 FS::svc_broadband - Object methods for svc_broadband records
21
22 =head1 SYNOPSIS
23
24   use FS::svc_broadband;
25
26   $record = new FS::svc_broadband \%hash;
27   $record = new FS::svc_broadband { 'column' => 'value' };
28
29   $error = $record->insert;
30
31   $error = $new_record->replace($old_record);
32
33   $error = $record->delete;
34
35   $error = $record->check;
36
37   $error = $record->suspend;
38
39   $error = $record->unsuspend;
40
41   $error = $record->cancel;
42
43 =head1 DESCRIPTION
44
45 An FS::svc_broadband object represents a 'broadband' Internet connection, such
46 as a DSL, cable modem, or fixed wireless link.  These services are assumed to
47 have the following properties:
48
49 FS::svc_broadband inherits from FS::svc_Common.  The following fields are
50 currently supported:
51
52 =over 4
53
54 =item svcnum - primary key
55
56 =item blocknum - see FS::addr_block
57
58 =item
59 speed_up - maximum upload speed, in bits per second.  If set to zero, upload
60 speed will be unlimited.  Exports that do traffic shaping should handle this
61 correctly, and not blindly set the upload speed to zero and kill the customer's
62 connection.
63
64 =item
65 speed_down - maximum download speed, as above
66
67 =item ip_addr - the customer's IP address.  If the customer needs more than one
68 IP address, set this to the address of the customer's router.  As a result, the
69 customer's router will have the same address for both its internal and external
70 interfaces thus saving address space.  This has been found to work on most NAT
71 routers available.
72
73 =back
74
75 =head1 METHODS
76
77 =over 4
78
79 =item new HASHREF
80
81 Creates a new svc_broadband.  To add the record to the database, see
82 "insert".
83
84 Note that this stores the hash reference, not a distinct copy of the hash it
85 points to.  You can ask the object for a copy with the I<hash> method.
86
87 =cut
88
89 sub table_info {
90   {
91     'name' => 'Broadband',
92     'name_plural' => 'Broadband services',
93     'longname_plural' => 'Fixed (username-less) broadband services',
94     'display_weight' => 50,
95     'cancel_weight'  => 70,
96     'fields' => {
97       'description' => 'Descriptive label for this particular device.',
98       'speed_down'  => 'Maximum download speed for this service in Kbps.  0 denotes unlimited.',
99       'speed_up'    => 'Maximum upload speed for this service in Kbps.  0 denotes unlimited.',
100       'ip_addr'     => 'IP address.  Leave blank for automatic assignment.',
101       'blocknum'    => { 'label' => 'Address block',
102                          'type'  => 'select',
103                          'select_table' => 'addr_block',
104                          'select_key'   => 'blocknum',
105                          'select_label' => 'cidr',
106                          'disable_inventory' => 1,
107                        },
108     },
109   };
110 }
111
112 sub table { 'svc_broadband'; }
113
114 sub table_dupcheck_fields { ( 'mac_addr' ); }
115
116 =item search HASHREF
117
118 Class method which returns a qsearch hash expression to search for parameters
119 specified in HASHREF.
120
121 Parameters:
122
123 =over 4
124
125 =item unlinked - set to search for all unlinked services.  Overrides all other options.
126
127 =item agentnum
128
129 =item custnum
130
131 =item svcpart
132
133 =item ip_addr
134
135 =item pkgpart - arrayref
136
137 =item routernum - arrayref
138
139 =item order_by
140
141 =back
142
143 =cut
144
145 sub search {
146   my ($class, $params) = @_;
147   my @where = ();
148   my @from = (
149     'LEFT JOIN cust_svc  USING ( svcnum  )',
150     'LEFT JOIN part_svc  USING ( svcpart )',
151     'LEFT JOIN cust_pkg  USING ( pkgnum  )',
152     'LEFT JOIN cust_main USING ( custnum )',
153   );
154
155   # based on FS::svc_acct::search, probably the most mature of the bunch
156   #unlinked
157   push @where, 'pkgnum IS NULL' if $params->{'unlinked'};
158   
159   #agentnum
160   if ( $params->{'agentnum'} =~ /^(\d+)$/ and $1 ) {
161     push @where, "agentnum = $1";
162   }
163   push @where, $FS::CurrentUser::CurrentUser->agentnums_sql(
164     'null_right' => 'View/link unlinked services',
165     'table' => 'cust_main'
166   );
167
168   #custnum
169   if ( $params->{'custnum'} =~ /^(\d+)$/ and $1 ) {
170     push @where, "custnum = $1";
171   }
172
173   #pkgpart, now properly untainted, can be arrayref
174   for my $pkgpart ( $params->{'pkgpart'} ) {
175     if ( ref $pkgpart ) {
176       my $where = join(',', map { /^(\d+)$/ ? $1 : () } @$pkgpart );
177       push @where, "cust_pkg.pkgpart IN ($where)" if $where;
178     }
179     elsif ( $pkgpart =~ /^(\d+)$/ ) {
180       push @where, "cust_pkg.pkgpart = $1";
181     }
182   }
183
184   #routernum, can be arrayref
185   for my $routernum ( $params->{'routernum'} ) {
186     push @from, 'LEFT JOIN addr_block USING ( blocknum )';
187     if ( ref $routernum and grep { $_ } @$routernum ) {
188       my $where = join(',', map { /^(\d+)$/ ? $1 : () } @$routernum );
189       push @where, "addr_block.routernum IN ($where)" if $where;
190     }
191     elsif ( $routernum =~ /^(\d+)$/ ) {
192       push @where, "addr_block.routernum = $1";
193     }
194   }
195  
196   #svcnum
197   if ( $params->{'svcnum'} =~ /^(\d+)$/ ) {
198     push @where, "svcnum = $1";
199   }
200
201   #svcpart
202   if ( $params->{'svcpart'} =~ /^(\d+)$/ ) {
203     push @where, "svcpart = $1";
204   }
205
206   #ip_addr
207   if ( $params->{'ip_addr'} =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/ ) {
208     push @where, "ip_addr = '$1'";
209   }
210
211   #custnum
212   if ( $params->{'custnum'} =~ /^(\d+)$/ and $1) {
213     push @where, "custnum = $1";
214   }
215   
216   my $addl_from = join(' ', @from);
217   my $extra_sql = '';
218   $extra_sql = 'WHERE '.join(' AND ', @where) if @where;
219   my $count_query = "SELECT COUNT(*) FROM svc_broadband $addl_from $extra_sql";
220   return( {
221       'table'   => 'svc_broadband',
222       'hashref' => {},
223       'select'  => join(', ',
224         'svc_broadband.*',
225         'part_svc.svc',
226         'cust_main.custnum',
227         FS::UI::Web::cust_sql_fields($params->{'cust_fields'}),
228       ),
229       'extra_sql' => $extra_sql,
230       'addl_from' => $addl_from,
231       'order_by'  => "ORDER BY ".($params->{'order_by'} || 'svcnum'),
232       'count_query' => $count_query,
233     } );
234 }
235
236 =item search_sql STRING
237
238 Class method which returns an SQL fragment to search for the given string.
239
240 =cut
241
242 sub search_sql {
243   my( $class, $string ) = @_;
244   if ( $string =~ /^(\d{1,3}\.){3}\d{1,3}$/ ) {
245     $class->search_sql_field('ip_addr', $string );
246   }elsif ( $string =~ /^([a-fA-F0-9]{12})$/ ) {
247     $class->search_sql_field('mac_addr', uc($string));
248   }elsif ( $string =~ /^(([a-fA-F0-9]{1,2}:){5}([a-fA-F0-9]{1,2}))$/ ) {
249     $class->search_sql_field('mac_addr', uc("$2$3$4$5$6$7") );
250   } else {
251     '1 = 0'; #false
252   }
253 }
254
255 =item label
256
257 Returns the IP address.
258
259 =cut
260
261 sub label {
262   my $self = shift;
263   $self->ip_addr;
264 }
265
266 =item insert [ , OPTION => VALUE ... ]
267
268 Adds this record to the database.  If there is an error, returns the error,
269 otherwise returns false.
270
271 The additional fields pkgnum and svcpart (see FS::cust_svc) should be 
272 defined.  An FS::cust_svc record will be created and inserted.
273
274 Currently available options are: I<depend_jobnum>
275
276 If I<depend_jobnum> is set (to a scalar jobnum or an array reference of
277 jobnums), all provisioning jobs will have a dependancy on the supplied
278 jobnum(s) (they will not run until the specific job(s) complete(s)).
279
280 =cut
281
282 # Standard FS::svc_Common::insert
283
284 =item delete
285
286 Delete this record from the database.
287
288 =cut
289
290 # Standard FS::svc_Common::delete
291
292 =item replace OLD_RECORD
293
294 Replaces the OLD_RECORD with this one in the database.  If there is an error,
295 returns the error, otherwise returns false.
296
297 =cut
298
299 # Standard FS::svc_Common::replace
300
301 =item suspend
302
303 Called by the suspend method of FS::cust_pkg (see FS::cust_pkg).
304
305 =item unsuspend
306
307 Called by the unsuspend method of FS::cust_pkg (see FS::cust_pkg).
308
309 =item cancel
310
311 Called by the cancel method of FS::cust_pkg (see FS::cust_pkg).
312
313 =item check
314
315 Checks all fields to make sure this is a valid broadband service.  If there is
316 an error, returns the error, otherwise returns false.  Called by the insert
317 and replace methods.
318
319 =cut
320
321 sub check {
322   my $self = shift;
323   my $x = $self->setfixed;
324
325   return $x unless ref($x);
326
327   my $error =
328     $self->ut_numbern('svcnum')
329     || $self->ut_numbern('blocknum')
330     || $self->ut_textn('description')
331     || $self->ut_number('speed_up')
332     || $self->ut_number('speed_down')
333     || $self->ut_ipn('ip_addr')
334     || $self->ut_hexn('mac_addr')
335     || $self->ut_hexn('auth_key')
336     || $self->ut_coordn('latitude', -90, 90)
337     || $self->ut_coordn('longitude', -180, 180)
338     || $self->ut_sfloatn('altitude')
339     || $self->ut_textn('vlan_profile')
340   ;
341   return $error if $error;
342
343   if($self->speed_up < 0) { return 'speed_up must be positive'; }
344   if($self->speed_down < 0) { return 'speed_down must be positive'; }
345
346   my $cust_svc = $self->svcnum
347                  ? qsearchs('cust_svc', { 'svcnum' => $self->svcnum } )
348                  : '';
349   my $cust_pkg;
350   if ($cust_svc) {
351     $cust_pkg = $cust_svc->cust_pkg;
352   }else{
353     $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $self->pkgnum } );
354     return "Invalid pkgnum" unless $cust_pkg;
355   }
356     
357   if ($self->blocknum) {
358     $error = $self->ut_foreign_key('blocknum', 'addr_block', 'blocknum');
359     return $error if $error;
360   }
361
362   if ($cust_pkg && $self->blocknum) {
363     my $addr_agentnum = $self->addr_block->agentnum;
364     if ($addr_agentnum && $addr_agentnum != $cust_pkg->cust_main->agentnum) {
365       return "Address block does not service this customer";
366     }
367   }
368
369   $error = $self->_check_ip_addr;
370   return $error if $error;
371
372   $self->SUPER::check;
373 }
374
375 sub _check_ip_addr {
376   my $self = shift;
377
378   if (not($self->ip_addr) or $self->ip_addr eq '0.0.0.0') {
379
380     return '' if $conf->exists('svc_broadband-allow_null_ip_addr'); #&& !$self->blocknum
381
382     return "Must supply either address or block"
383       unless $self->blocknum;
384     my $next_addr = $self->addr_block->next_free_addr;
385     if ($next_addr) {
386       $self->ip_addr($next_addr->addr);
387     } else {
388       return "No free addresses in addr_block (blocknum: ".$self->blocknum.")";
389     }
390
391   }
392
393   if (not($self->blocknum)) {
394     return "Must supply either address or block"
395       unless ($self->ip_addr and $self->ip_addr ne '0.0.0.0');
396     my @block = grep { $_->NetAddr->contains($self->NetAddr) }
397                  map { $_->addr_block }
398                  $self->allowed_routers;
399     if (scalar(@block)) {
400       $self->blocknum($block[0]->blocknum);
401     }else{
402       return "Address not with available block.";
403     }
404   }
405
406   # This should catch errors in the ip_addr.  If it doesn't,
407   # they'll almost certainly not map into the block anyway.
408   my $self_addr = $self->NetAddr; #netmask is /32
409   return ('Cannot parse address: ' . $self->ip_addr) unless $self_addr;
410
411   my $block_addr = $self->addr_block->NetAddr;
412   unless ($block_addr->contains($self_addr)) {
413     return 'blocknum '.$self->blocknum.' does not contain address '.$self->ip_addr;
414   }
415
416   my $router = $self->addr_block->router 
417     or return 'Cannot assign address from unallocated block:'.$self->addr_block->blocknum;
418   if(grep { $_->routernum == $router->routernum} $self->allowed_routers) {
419   } # do nothing
420   else {
421     return 'Router '.$router->routernum.' cannot provide svcpart '.$self->svcpart;
422   }
423
424   '';
425 }
426
427 sub _check_duplicate {
428   my $self = shift;
429
430   return "MAC already in use"
431     if ( $self->mac_addr &&
432          scalar( qsearch( 'svc_broadband', { 'mac_addr', $self->mac_addr } ) )
433        );
434
435   '';
436 }
437
438
439 =item NetAddr
440
441 Returns a NetAddr::IP object containing the IP address of this service.  The netmask 
442 is /32.
443
444 =cut
445
446 sub NetAddr {
447   my $self = shift;
448   new NetAddr::IP ($self->ip_addr);
449 }
450
451 =item addr_block
452
453 Returns the FS::addr_block record (i.e. the address block) for this broadband service.
454
455 =cut
456
457 sub addr_block {
458   my $self = shift;
459   qsearchs('addr_block', { blocknum => $self->blocknum });
460 }
461
462 =back
463
464 =item allowed_routers
465
466 Returns a list of allowed FS::router objects.
467
468 =cut
469
470 sub allowed_routers {
471   my $self = shift;
472   map { $_->router } qsearch('part_svc_router', { svcpart => $self->svcpart });
473 }
474
475 =head1 BUGS
476
477 The business with sb_field has been 'fixed', in a manner of speaking.
478
479 allowed_routers isn't agent virtualized because part_svc isn't agent
480 virtualized
481
482 =head1 SEE ALSO
483
484 FS::svc_Common, FS::Record, FS::addr_block,
485 FS::part_svc, schema.html from the base documentation.
486
487 =cut
488
489 1;
490