get the dup checking right
[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_sql STRING
117
118 Class method which returns an SQL fragment to search for the given string.
119
120 =cut
121
122 sub search_sql {
123   my( $class, $string ) = @_;
124   if ( $string =~ /^(\d{1,3}\.){3}\d{1,3}$/ ) {
125     $class->search_sql_field('ip_addr', $string );
126   }elsif ( $string =~ /^([a-fA-F0-9]{12})$/ ) {
127     $class->search_sql_field('mac_addr', uc($string));
128   }elsif ( $string =~ /^(([a-fA-F0-9]{1,2}:){5}([a-fA-F0-9]{1,2}))$/ ) {
129     $class->search_sql_field('mac_addr', uc("$2$3$4$5$6$7") );
130   } else {
131     '1 = 0'; #false
132   }
133 }
134
135 =item label
136
137 Returns the IP address.
138
139 =cut
140
141 sub label {
142   my $self = shift;
143   $self->ip_addr;
144 }
145
146 =item insert [ , OPTION => VALUE ... ]
147
148 Adds this record to the database.  If there is an error, returns the error,
149 otherwise returns false.
150
151 The additional fields pkgnum and svcpart (see FS::cust_svc) should be 
152 defined.  An FS::cust_svc record will be created and inserted.
153
154 Currently available options are: I<depend_jobnum>
155
156 If I<depend_jobnum> is set (to a scalar jobnum or an array reference of
157 jobnums), all provisioning jobs will have a dependancy on the supplied
158 jobnum(s) (they will not run until the specific job(s) complete(s)).
159
160 =cut
161
162 # Standard FS::svc_Common::insert
163
164 =item delete
165
166 Delete this record from the database.
167
168 =cut
169
170 # Standard FS::svc_Common::delete
171
172 =item replace OLD_RECORD
173
174 Replaces the OLD_RECORD with this one in the database.  If there is an error,
175 returns the error, otherwise returns false.
176
177 =cut
178
179 # Standard FS::svc_Common::replace
180
181 =item suspend
182
183 Called by the suspend method of FS::cust_pkg (see FS::cust_pkg).
184
185 =item unsuspend
186
187 Called by the unsuspend method of FS::cust_pkg (see FS::cust_pkg).
188
189 =item cancel
190
191 Called by the cancel method of FS::cust_pkg (see FS::cust_pkg).
192
193 =item check
194
195 Checks all fields to make sure this is a valid broadband service.  If there is
196 an error, returns the error, otherwise returns false.  Called by the insert
197 and replace methods.
198
199 =cut
200
201 sub check {
202   my $self = shift;
203   my $x = $self->setfixed;
204
205   return $x unless ref($x);
206
207   my $error =
208     $self->ut_numbern('svcnum')
209     || $self->ut_numbern('blocknum')
210     || $self->ut_textn('description')
211     || $self->ut_number('speed_up')
212     || $self->ut_number('speed_down')
213     || $self->ut_ipn('ip_addr')
214     || $self->ut_hexn('mac_addr')
215     || $self->ut_hexn('auth_key')
216     || $self->ut_coordn('latitude', -90, 90)
217     || $self->ut_coordn('longitude', -180, 180)
218     || $self->ut_sfloatn('altitude')
219     || $self->ut_textn('vlan_profile')
220   ;
221   return $error if $error;
222
223   if($self->speed_up < 0) { return 'speed_up must be positive'; }
224   if($self->speed_down < 0) { return 'speed_down must be positive'; }
225
226   my $cust_svc = $self->svcnum
227                  ? qsearchs('cust_svc', { 'svcnum' => $self->svcnum } )
228                  : '';
229   my $cust_pkg;
230   if ($cust_svc) {
231     $cust_pkg = $cust_svc->cust_pkg;
232   }else{
233     $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $self->pkgnum } );
234     return "Invalid pkgnum" unless $cust_pkg;
235   }
236     
237   if ($self->blocknum) {
238     $error = $self->ut_foreign_key('blocknum', 'addr_block', 'blocknum');
239     return $error if $error;
240   }
241
242   if ($cust_pkg && $self->blocknum) {
243     my $addr_agentnum = $self->addr_block->agentnum;
244     if ($addr_agentnum && $addr_agentnum != $cust_pkg->cust_main->agentnum) {
245       return "Address block does not service this customer";
246     }
247   }
248
249   if (not($self->ip_addr) or $self->ip_addr eq '0.0.0.0') {
250     return "Must supply either address or block"
251       unless $self->blocknum;
252     my $next_addr = $self->addr_block->next_free_addr;
253     if ($next_addr) {
254       $self->ip_addr($next_addr->addr);
255     } else {
256       return "No free addresses in addr_block (blocknum: ".$self->blocknum.")";
257     }
258   }
259
260   if (not($self->blocknum)) {
261     return "Must supply either address or block"
262       unless ($self->ip_addr and $self->ip_addr ne '0.0.0.0');
263     my @block = grep { $_->NetAddr->contains($self->NetAddr) }
264                  map { $_->addr_block }
265                  $self->allowed_routers;
266     if (scalar(@block)) {
267       $self->blocknum($block[0]->blocknum);
268     }else{
269       return "Address not with available block.";
270     }
271   }
272
273   # This should catch errors in the ip_addr.  If it doesn't,
274   # they'll almost certainly not map into the block anyway.
275   my $self_addr = $self->NetAddr; #netmask is /32
276   return ('Cannot parse address: ' . $self->ip_addr) unless $self_addr;
277
278   my $block_addr = $self->addr_block->NetAddr;
279   unless ($block_addr->contains($self_addr)) {
280     return 'blocknum '.$self->blocknum.' does not contain address '.$self->ip_addr;
281   }
282
283   my $router = $self->addr_block->router 
284     or return 'Cannot assign address from unallocated block:'.$self->addr_block->blocknum;
285   if(grep { $_->routernum == $router->routernum} $self->allowed_routers) {
286   } # do nothing
287   else {
288     return 'Router '.$router->routernum.' cannot provide svcpart '.$self->svcpart;
289   }
290
291   $self->SUPER::check;
292 }
293
294 sub _check_duplicate {
295   my $self = shift;
296
297   return "MAC already in use"
298     if ( $self->mac_addr &&
299          scalar( qsearch( 'svc_broadband', { 'mac_addr', $self->mac_addr } ) )
300        );
301
302   '';
303 }
304
305
306 =item NetAddr
307
308 Returns a NetAddr::IP object containing the IP address of this service.  The netmask 
309 is /32.
310
311 =cut
312
313 sub NetAddr {
314   my $self = shift;
315   new NetAddr::IP ($self->ip_addr);
316 }
317
318 =item addr_block
319
320 Returns the FS::addr_block record (i.e. the address block) for this broadband service.
321
322 =cut
323
324 sub addr_block {
325   my $self = shift;
326   qsearchs('addr_block', { blocknum => $self->blocknum });
327 }
328
329 =back
330
331 =item allowed_routers
332
333 Returns a list of allowed FS::router objects.
334
335 =cut
336
337 sub allowed_routers {
338   my $self = shift;
339   map { $_->router } qsearch('part_svc_router', { svcpart => $self->svcpart });
340 }
341
342 =head1 BUGS
343
344 The business with sb_field has been 'fixed', in a manner of speaking.
345
346 allowed_routers isn't agent virtualized because part_svc isn't agent
347 virtualized
348
349 =head1 SEE ALSO
350
351 FS::svc_Common, FS::Record, FS::addr_block,
352 FS::part_svc, schema.html from the base documentation.
353
354 =cut
355
356 1;
357