Merge branch 'master' of git.freeside.biz:/home/git/freeside
[freeside.git] / FS / FS / part_export / saisei.pm
1 package FS::part_export::saisei;
2
3 use strict;
4 use vars qw( @ISA %info );
5 use base qw( FS::part_export );
6 use Date::Format 'time2str';
7 use Cpanel::JSON::XS;
8 use MIME::Base64;
9 use REST::Client;
10 use Data::Dumper;
11 use FS::Conf;
12 use Carp qw(carp);
13
14 =pod
15
16 =head1 NAME
17
18 FS::part_export::saisei
19
20 =head1 SYNOPSIS
21
22 Saisei integration for Freeside
23
24 =head1 DESCRIPTION
25
26 This export offers basic svc_broadband provisioning for Saisei.
27
28 This is a customer integration with Saisei.  This will set up a rate plan and tie
29 the rate plan to a host and the access point via the Saisei API when the broadband service is provisioned.
30 It will also untie the host from the rate plan, setting it to the default rate plan via the API upon unprovisioning of the broadband service.
31
32 This will create and modify the rate plans at Saisei as soon as the broadband service attached to this export is created or modified.
33 This will also create and modify an access point at Saisei as soon as the tower is created or modified.
34
35 To use this export, follow the below instructions:
36
37 Create a new service definition and set the table to svc_broadband.  The service name will become the Saisei rate plan name.
38 Set the upload and download speed for the service. This is required to be able to export the service to Saisei.
39 Attach this Saisei export to this service.
40
41 Create a tower and add a sector to that tower.  The sector name will be the name of the access point,
42 Make sure you have set the up and down rate limit for the tower and the sector.  This is required to be able to export the access point.
43 The tower and sector will be set up as access points at Saisei upon the creation of the tower or sector.  They will be modified at Saisei when modified in freeside.
44 Each sector will be attached to its tower access point using the Saisei uplink field.
45 Each access point will be attached to the interface set in the export config.  If left blank access point will be attached to the default interface.  Most setups can leave this blank.
46
47 Create a package for the above created service, and order this package for a customer.
48
49 Provision the service, making sure to enter the IP address associated with this service and select the tower and sector for it's access point.
50 This provisioned service will then be exported as a host to Saisei.
51
52 Unprovisioning this service will set the host entry at Saisei to the default rate plan with the user and access point set to <none>.
53
54 After this export is set up and attached to a service, you can export the already provisioned services by clicking the link Export provisioned services attached to this export.
55 Clicking on this link will export all services attached to this export not currently exported to Saisei.
56
57 This module also provides generic methods for working through the L</Saisei API>.
58
59 =cut
60
61 tie my %scripts, 'Tie::IxHash',
62   'export_provisioned_services'  => { component => '/elements/popup_link.html',
63                                       label     => 'Export provisioned services',
64                                       description => 'will export provisioned services of part service with Saisei export attached.',
65                                       html_label => '<b>Export provisioned services attached to this export.</b>',
66                                       error_url  => '/edit/part_export.cgi?',
67                                       success_message => 'Saisei export of provisioned services successful',
68                                     },
69   'export_all_towers_sectors'    => { component => '/elements/popup_link.html',
70                                       label     => 'Export of all towers and sectors',
71                                       description => 'Will force an export of all towers and sectors to Saisei as access points.',
72                                       html_label => '<b>Export all towers and sectors.</b>',
73                                       error_url  => '/edit/part_export.cgi?',
74                                       success_message => 'Saisei export of towers and sectors as access points successful',
75                                     },
76   'force_export_all_users'       => { component => '/elements/popup_link.html',
77                                       label     => 'Force update of all Saisei users from freeside provisioned services',
78                                       description => 'Will force an update of Saisei users description and map location from freeside provisioned services.',
79                                       html_label => '<b>Force update of all Saisei users from freeside provisioned services</b>',
80                                       error_url  => '/edit/part_export.cgi?',
81                                       success_message => 'Export of freeside provisioned services as Saisei users was successful',
82                                     },
83   'force_export_all_virtual_ap'  => { component => '/elements/popup_link.html',
84                                       label     => 'Force update of all virtual Access Points',
85                                       description => 'Will force an update of all virtual access points.',
86                                       html_label => '<b>Force update of all virtual Access Points</b>',
87                                       error_url  => '/edit/part_export.cgi?',
88                                       success_message => 'Export of all virtual access points to Saisei was successful',
89                                     },
90 ;
91
92 tie my %options, 'Tie::IxHash',
93   'port'             => { label => 'Port',
94                           default => 5000 },
95   'username'         => { label => 'Saisei API User Name',
96                           default => '' },
97   'password'         => { label => 'Saisei API Password',
98                           default => '' },
99   'interface'        => { label => 'Saisei Access Point Interface',
100                           default => '' },
101   'debug'            => { type => 'checkbox',
102                           label => 'Enable debug warnings' },
103 ;
104
105 %info = (
106   'svc'             => 'svc_broadband',
107   'desc'            => 'Export broadband service/account to Saisei',
108   'options'         => \%options,
109   'scripts'         => \%scripts,
110   'notes'           => <<'END',
111 This is a customer integration with Saisei.  This will set up a rate plan and tie
112 the rate plan to a host and the access point via the Saisei API when the broadband service is provisioned.
113 It will also untie the host from the rate plan, setting it to the default rate plan via the API upon unprovisioning of the broadband service.
114 <P>
115 This will create and modify the rate plans at Saisei as soon as the broadband service attached to this export is created or modified.
116 This will also create and modify an access point at Saisei as soon as the tower is created or modified.
117 <P>
118 To use this export, follow the below instructions:
119 <P>
120 <OL>
121 <LI>
122 Create a new service definition and set the table to svc_broadband.  The service name will become the Saisei rate plan name.
123 Set the upload speed, download speed, and tower to be required for the service. This is required to be able to export the service to Saisei.
124 Attach this Saisei export to this service.
125 </LI>
126 <P>
127 <LI>
128 Create a tower and add a sector to that tower.  The sector name will be the name of the access point,
129 Make sure you have set the up and down rate limit for the tower and the sector.  This is required to be able to export the access point.
130 The tower and sector will be set up as access points at Saisei upon the creation of the tower or sector.  They will be modified at Saisei when modified in freeside.
131 Each sector will be attached to its tower access point using the Saisei uplink field.
132 Each access point will be attached to the interface set in the export config.  If left blank access point will be attached to the default interface.  Most setups can leave this blank.
133 </LI>
134 <P>
135 <LI>
136 Create a package for the above created service, and order this package for a customer.
137 </LI>
138 <P>
139 <LI>
140 Provision the service, making sure to enter the IP address associated with this service, the upload and download speed are correct, and select the tower and sector for it's access point.
141 This provisioned service will then be exported as a host to Saisei.
142 <P>
143 Unprovisioning this service will set the host entry at Saisei to the default rate plan with the user and access point set to <i>none</i>.
144 </LI>
145 <P>
146 <LI>
147 After this export is set up and attached to a service, you can export the already provisioned services by clicking the link <b>Export provisioned services attached to this export</b>.
148 Clicking on this link will export all services attached to this export not currently exported to Saisei.
149 </LI>
150 </OL>
151 <P>
152 <A HREF="http://www.freeside.biz/mediawiki/index.php/Saisei_provisioning_export" target="_new">Documentation</a>
153 END
154 );
155
156 sub _export_insert {
157   my ($self, $svc_broadband, $force_update) = @_;
158
159   my $rateplan_name = $self->get_rateplan_name($svc_broadband);
160
161   # check for existing rate plan
162   my $existing_rateplan;
163   $existing_rateplan = $self->api_get_rateplan($rateplan_name) unless $self->{'__saisei_error'};
164
165   die ("Please double check your credentials as ".$existing_rateplan->{message}."\n") if $existing_rateplan->{message};
166
167   # if no existing rate plan create one and modify it.
168   $self->api_create_rateplan($svc_broadband, $rateplan_name) unless $existing_rateplan->{collection};
169   $self->api_modify_rateplan($svc_broadband, $rateplan_name) unless ($self->{'__saisei_error'} || $existing_rateplan->{collection});
170   return $self->api_error if $self->{'__saisei_error'};
171
172   # set rateplan to existing one or newly created one.
173   my $rateplan = $existing_rateplan->{collection} ? $existing_rateplan : $self->api_get_rateplan($rateplan_name);
174
175   my $username = $svc_broadband->{Hash}->{svcnum};
176   my $description = $svc_broadband->{Hash}->{description};
177   my $svc_location = get_svc_location($self, $svc_broadband);
178
179   if (!$username) {
180     $self->{'__saisei_error'} = 'no username - can not export';
181     return $self->api_error;
182   }
183   else {
184     # check for existing user.
185     my $existing_user;
186     $existing_user = $self->api_get_user($username) unless $self->{'__saisei_error'};
187  
188     # if no existing user create one.
189     $self->api_create_user($username, $description, $svc_location) unless $existing_user;
190     return $self->api_error if $self->{'__saisei_error'};
191
192     # set user to existing one or newly created one.
193     my $user = $existing_user ? $existing_user : $self->api_get_user($username);
194
195     ## add access point
196     my $tower_sector = FS::Record::qsearchs({
197       'table'     => 'tower_sector',
198       'select'    => 'tower.towername,
199                       tower.up_rate_limit as tower_upratelimit,
200                       tower.down_rate_limit as tower_downratelimit,
201                       tower_sector.sectorname,
202                       tower_sector.towernum,
203                       tower_sector.up_rate_limit as sector_upratelimit,
204                       tower_sector.down_rate_limit as sector_downratelimit,
205                       tower.latitude,
206                       tower.longitude',
207       'addl_from' => 'LEFT JOIN tower USING ( towernum )',
208       'hashref'   => {
209                         'sectornum' => $svc_broadband->{Hash}->{sectornum},
210                      },
211     });
212
213     my $tower_location;
214     $tower_location = $tower_sector->{Hash}->{latitude}.','.$tower_sector->{Hash}->{longitude} if ($tower_sector->{Hash}->{latitude} && $tower_sector->{Hash}->{longitude});
215
216     my $tower_name = $tower_sector->{Hash}->{towername};
217     $tower_name =~ s/\s/_/g;
218
219     my $tower_opt = {
220       'tower_name'           => $tower_name,
221       'tower_num'            => $tower_sector->{Hash}->{towernum},
222       'tower_uprate_limit'   => $tower_sector->{Hash}->{tower_upratelimit},
223       'tower_downrate_limit' => $tower_sector->{Hash}->{tower_downratelimit},
224     };
225     $tower_opt->{'location'} = $tower_location if $tower_location;
226
227     my $tower_ap = process_tower($self, $tower_opt);
228     return $self->api_error if $self->{'__saisei_error'};
229
230     my $sector_name = $tower_sector->{Hash}->{sectorname};
231     $sector_name =~ s/\s/_/g;
232
233     my $sector_opt = {
234       'tower_name'            => $tower_name,
235       'tower_num'             => $tower_sector->{Hash}->{towernum},
236       'sector_name'           => $sector_name,
237       'sector_uprate_limit'   => $tower_sector->{Hash}->{sector_upratelimit},
238       'sector_downrate_limit' => $tower_sector->{Hash}->{sector_downratelimit},
239       'rateplan'              => $rateplan_name,
240     };
241     $sector_opt->{'location'} = $tower_location if $tower_location;
242
243     my $accesspoint = process_sector($self, $sector_opt);
244     return $self->api_error if $self->{'__saisei_error'};
245
246 ## get custnum and pkgpart from cust_pkg for virtual access point
247     my $cust_pkg = FS::Record::qsearchs({
248       'table'     => 'cust_pkg',
249       'hashref'   => { 'pkgnum' => $svc_broadband->{Hash}->{pkgnum}, },
250     });
251
252     my $virtual_ap_name = $cust_pkg->{Hash}->{custnum}.'_'.$cust_pkg->{Hash}->{pkgpart}.'_'.$svc_broadband->{Hash}->{speed_down}.'_'.$svc_broadband->{Hash}->{speed_up};
253     my $modify_existing_virtual_ap = '1' if $force_update->{'update_virtual_ap'};
254
255     my $virtual_ap_opt = {
256       'virtual_name'           => $virtual_ap_name,
257       'sector_name'            => $sector_name,
258       'virtual_uprate_limit'   => $svc_broadband->{Hash}->{speed_up},
259       'virtual_downrate_limit' => $svc_broadband->{Hash}->{speed_down},
260       'location'               => $svc_location,
261       'modify_existing'        => $modify_existing_virtual_ap,
262     };
263     my $virtual_ap = process_virtual_ap($self, $virtual_ap_opt);
264     return $self->api_error if $self->{'__saisei_error'};
265
266     ## tie host to user add sector name as access point.
267     my $host_opt = {
268       'user'        => $user->{collection}->[0]->{name},
269       'rateplan'    => $rateplan->{collection}->[0]->{name},
270       'ip'          => $svc_broadband->{Hash}->{ip_addr},
271       'accesspoint' => $virtual_ap->{collection}->[0]->{name},
272       'location'    => $svc_location,
273     };
274     $self->api_add_host_to_user($host_opt)
275       unless $self->{'__saisei_error'};
276   }
277
278   return $self->api_error;
279
280 }
281
282 sub _export_replace {
283   my ($self, $svc_broadband) = @_;
284   my $error = $self->_export_insert($svc_broadband);
285   return $error;
286 }
287
288 sub _export_delete {
289   my ($self, $svc_broadband) = @_;
290
291   my $rateplan_name = $self->get_rateplan_name($svc_broadband);
292
293   my $username = $svc_broadband->{Hash}->{svcnum};
294
295   ## untie host to user
296   $self->api_delete_host_to_user($username, $rateplan_name, $svc_broadband->{Hash}->{ip_addr}) unless $self->{'__saisei_error'};
297
298   return '';
299 }
300
301 sub _export_suspend {
302   my ($self, $svc_broadband) = @_;
303   return '';
304 }
305
306 sub _export_unsuspend {
307   my ($self, $svc_broadband) = @_;
308   return '';
309 }
310
311 sub export_partsvc {
312   my ($self, $svc_part) = @_;
313
314   if ( $FS::svc_Common::noexport_hack ) {
315     carp 'export_partsvc() suppressed by noexport_hack'
316       if $self->option('debug');
317     return;
318   }
319
320   my $fcc_477_speeds;
321   if ($svc_part->{Hash}->{svc_broadband__speed_down} eq "down" || $svc_part->{Hash}->{svc_broadband__speed_up} eq "up") {
322     for my $type (qw( down up )) {
323       my $speed_type = "broadband_".$type."stream";
324       foreach my $pkg_svc (FS::Record::qsearch({
325         'table'     => 'pkg_svc',
326         'select'    => 'pkg_svc.*, part_pkg_fcc_option.fccoptionname, part_pkg_fcc_option.optionvalue',
327         'addl_from' => ' LEFT JOIN part_pkg_fcc_option USING (pkgpart) ',
328         'extra_sql' => " WHERE pkg_svc.svcpart = ".$svc_part->{Hash}->{svcpart}." AND pkg_svc.quantity > 0 AND part_pkg_fcc_option.fccoptionname = '".$speed_type."'",
329       })) { $fcc_477_speeds->{
330         $pkg_svc->{Hash}->{pkgpart}}->{$speed_type} = $pkg_svc->{Hash}->{optionvalue} * 1000 unless !$pkg_svc->{Hash}->{optionvalue}; }
331     }
332   }
333   else {
334     $fcc_477_speeds->{1}->{broadband_downstream} = $svc_part->{Hash}->{"svc_broadband__speed_down"};
335     $fcc_477_speeds->{1}->{broadband_upstream} = $svc_part->{Hash}->{"svc_broadband__speed_up"};
336   }
337
338   foreach my $key (keys %$fcc_477_speeds) {
339
340     $svc_part->{Hash}->{speed_down} = $fcc_477_speeds->{$key}->{broadband_downstream};
341     $svc_part->{Hash}->{speed_up} = $fcc_477_speeds->{$key}->{broadband_upstream};
342     $svc_part->{Hash}->{svc_broadband__speed_down} = $fcc_477_speeds->{$key}->{broadband_downstream};
343     $svc_part->{Hash}->{svc_broadband__speed_up} = $fcc_477_speeds->{$key}->{broadband_upstream};
344
345     my $temp_svc = $svc_part->{Hash};
346     my $svc_broadband = {};
347     map { if ($_ =~ /^svc_broadband__(.*)$/) { $svc_broadband->{Hash}->{$1} = $temp_svc->{$_}; }  } keys %$temp_svc;
348
349     my $rateplan_name = $self->get_rateplan_name($svc_broadband, $svc_part->{Hash}->{svc});
350
351     # check for existing rate plan
352     my $existing_rateplan;
353     $existing_rateplan = $self->api_get_rateplan($rateplan_name) unless $self->{'__saisei_error'};
354
355     # Modify the existing rate plan with new service data.
356     $self->api_modify_existing_rateplan($svc_broadband, $rateplan_name) unless ($self->{'__saisei_error'} || !$existing_rateplan);
357
358     # if no existing rate plan create one and modify it.
359     $self->api_create_rateplan($svc_broadband, $rateplan_name) unless $existing_rateplan;
360     $self->api_modify_rateplan($svc_part, $rateplan_name) unless ($self->{'__saisei_error'} || $existing_rateplan);
361
362   }
363
364   return $self->api_error;
365
366 }
367
368 sub export_tower_sector {
369   my ($self, $tower) = @_;
370
371   if ( $FS::svc_Common::noexport_hack ) {
372     carp 'export_tower_sector() suppressed by noexport_hack'
373       if $self->option('debug');
374     return;
375   }
376
377   my $tower_location;
378   $tower_location = $tower->{Hash}->{latitude}.','.$tower->{Hash}->{longitude} if ($tower->{Hash}->{latitude} && $tower->{Hash}->{longitude});
379
380   #modify tower or create it.
381   my $tower_name = $tower->{Hash}->{towername};
382   $tower_name =~ s/\s/_/g;
383   my $tower_opt = {
384     'tower_name'           => $tower_name,
385     'tower_num'            => $tower->{Hash}->{towernum},
386     'tower_uprate_limit'   => $tower->{Hash}->{up_rate_limit},
387     'tower_downrate_limit' => $tower->{Hash}->{down_rate_limit},
388     'modify_existing'      => '1', # modify an existing access point with this info
389   };
390   $tower_opt->{'location'} = $tower_location if $tower_location;
391
392   my $tower_access_point = process_tower($self, $tower_opt);
393     return $tower_access_point if $tower_access_point->{error};
394
395   #get list of all access points
396   my $hash_opt = {
397       'table'     => 'tower_sector',
398       'select'    => '*',
399       'hashref'   => { 'towernum' => $tower->{Hash}->{towernum}, },
400   };
401
402   #for each one modify or create it.
403   foreach my $tower_sector ( FS::Record::qsearch($hash_opt) ) {
404     next if $tower_sector->{Hash}->{sectorname} eq "_default";
405     my $sector_name = $tower_sector->{Hash}->{sectorname};
406     $sector_name =~ s/\s/_/g;
407     my $sector_opt = {
408       'tower_name'            => $tower_name,
409       'tower_num'             => $tower_sector->{Hash}->{towernum},
410       'sector_name'           => $sector_name,
411       'sector_uprate_limit'   => $tower_sector->{Hash}->{up_rate_limit},
412       'sector_downrate_limit' => $tower_sector->{Hash}->{down_rate_limit},
413       'modify_existing'       => '1', # modify an existing access point with this info
414     };
415     $sector_opt->{'location'} = $tower_location if $tower_location;
416
417     my $sector_access_point = process_sector($self, $sector_opt) unless ($sector_name eq "_default");
418       return $sector_access_point if $sector_access_point->{error};
419   }
420
421   return { error => $self->api_error, };
422 }
423
424 sub export_user {
425   my ($self, $username, $description, $location) = @_;
426
427   $self->api_create_user($username, $description, $location);
428
429   return $self->api_error if $self->{'__saisei_error'};
430
431   return '';
432 }
433
434 ## creates the rateplan name
435 sub get_rateplan_name {
436   my ($self, $svc_broadband, $svc_name) = @_;
437
438   my $service_part = FS::Record::qsearchs( 'part_svc', { 'svcpart' => $svc_broadband->{Hash}->{svcpart} } ) unless $svc_name;
439   my $service_name = $svc_name ? $svc_name : $service_part->{Hash}->{svc};
440
441   my $rateplan_name = $service_name . " " . $svc_broadband->{Hash}->{speed_down} . "-" . $svc_broadband->{Hash}->{speed_up};
442   $rateplan_name =~ s/\s/_/g; $rateplan_name =~ s/[^A-Za-z0-9\-_]//g;
443
444   return $rateplan_name;
445 }
446
447 =head1 Saisei API
448
449 These methods allow access to the Saisei API using the credentials
450 set in the export options.
451
452 =cut
453
454 =head2 api_call
455
456 Accepts I<$method>, I<$path>, I<$params> hashref and optional.
457 Places an api call to the specified path and method with the specified params.
458 Returns the decoded json object returned by the api call.
459 Returns empty on failure;  retrieve error messages using L</api_error>.
460
461 =cut
462
463 sub api_call {
464   my ($self,$method,$path,$params) = @_;
465
466   $self->{'__saisei_error'} = '';
467   my $auth_info = $self->option('username') . ':' . $self->option('password');
468   $params ||= {};
469
470   warn "Calling $method on http://"
471     .$self->{Hash}->{machine}.':'.$self->option('port')
472     ."/rest/top/configurations/running/$path\n" if $self->option('debug');
473
474   my $data = encode_json($params) if keys %{ $params };
475
476   my $client = REST::Client->new();
477   $client->addHeader("Authorization", "Basic ".encode_base64($auth_info));
478   $client->setHost('http://'.$self->{Hash}->{machine}.':'.$self->option('port'));
479   $client->$method('/rest/top/configurations/running'.$path, $data, { "Content-type" => 'application/json'});
480
481   warn "Saisei Response Code is ".$client->responseCode()."\n" if $self->option('debug');
482
483   my $result;
484
485   if ($client->responseCode() eq '200' || $client->responseCode() eq '201') {
486     eval { $result = decode_json($client->responseContent()) };
487     unless ($result) {
488       $self->{'__saisei_error'} = "There was an error decoding the JSON data from Saisei.  Bad JSON data logged in error log if debug option was set.";
489       warn "Saisei RC 201 Response Content is not json\n".$client->responseContent()."\n" if $self->option('debug');
490       return;
491     }
492   }
493   elsif ($client->responseCode() eq '404') {
494     eval { $result = decode_json($client->responseContent()) };
495     unless ($result) {
496       $self->{'__saisei_error'} = "There was an error decoding the JSON data from Saisei.  Bad JSON data logged in error log if debug option was set.";
497       warn "Saisei RC 404 Response Content is not json\n".$client->responseContent()."\n" if $self->option('debug');
498       return;
499     }
500     ## check if message is for empty hash.
501     my($does_not_exist) = $result->{message} =~ /'(.*)' does not exist$/;
502     $self->{'__saisei_error'} = "Saisei Error: ".$result->{message} unless $does_not_exist;
503     warn "Saisei Response Content is\n".$client->responseContent."\n" if ($self->option('debug') && !$does_not_exist);
504     return;
505   }
506   elsif ($client->responseCode() eq '500') {
507     $self->{'__saisei_error'} = "Could not connect to the Saisei export host machine (".$self->{Hash}->{machine}.':'.$self->option('port').") during $method , we received the responce code: " . $client->responseCode();
508     warn "Saisei Response Content is\n".$client->responseContent."\n" if $self->option('debug');
509     return;
510   }
511   else {
512     $self->{'__saisei_error'} = "Received Bad response from server during $method $path $data, we received responce code: " . $client->responseCode() . " " . $client->responseContent;
513     warn "Saisei Response Content is\n".$client->responseContent."\n" if $self->option('debug');
514     return; 
515   }
516
517   return $result;
518   
519 }
520
521 =head2 api_error
522
523 Returns the error string set by L</Saisei API> methods,
524 or a blank string if most recent call produced no errors.
525
526 =cut
527
528 sub api_error {
529   my $self = shift;
530   return $self->{'__saisei_error'} || '';
531 }
532
533 =head2 api_get_policies
534
535 Gets a list of global policies.
536
537 =cut
538
539 sub api_get_policies {
540   my $self = shift;
541
542   my $get_policies = $self->api_call("GET", '/policies/?token=1&order=name&start=0&limit=20&select=name%2Cpercent_rate%2Cassured%2C');
543   return if $self->api_error;
544   $self->{'__saisei_error'} = "Did not receive any global policies from Saisei."
545     unless $get_policies;
546
547   return $get_policies->{collection};
548 }
549
550 =head2 api_get_rateplan
551
552 Gets rateplan info for specific rateplan.
553
554 =cut
555
556 sub api_get_rateplan {
557   my $self = shift;
558   my $rateplan = shift;
559
560   my $get_rateplan = $self->api_call("GET", "/rate_plans/$rateplan");
561   return if $self->api_error;
562
563   return $get_rateplan;
564 }
565
566 =head2 api_get_user
567
568 Gets user info for specific user.
569
570 =cut
571
572 sub api_get_user {
573   my $self = shift;
574   my $user = shift;
575
576   my $get_user = $self->api_call("GET", "/users/$user");
577   return if $self->api_error;
578
579   return $get_user;
580 }
581
582 =head2 api_get_accesspoint
583
584 Gets user info for specific access point.
585
586 =cut
587
588 sub api_get_accesspoint {
589   my $self = shift;
590   my $accesspoint = shift;
591
592   my $get_accesspoint = $self->api_call("GET", "/access_points/$accesspoint");
593   return if $self->api_error;
594
595   return $get_accesspoint;
596 }
597
598 =head2 api_get_host
599
600 Gets user info for specific host.
601
602 =cut
603
604 sub api_get_host {
605   my $self = shift;
606   my $ip = shift;
607
608   my $get_host = $self->api_call("GET", "/hosts/$ip");
609
610   return { message => $self->api_error, } if $self->api_error;
611
612   return $get_host;
613 }
614
615 =head2 api_create_rateplan
616
617 Creates a rateplan.
618
619 =cut
620
621 sub api_create_rateplan {
622   my ($self, $svc, $rateplan) = @_;
623
624   $self->{'__saisei_error'} = "There is no download speed set for the service !--service,".$svc->{Hash}->{svcnum}.",".$rateplan."--! with host (".$svc->{Hash}->{ip_addr}."). All services that are to be exported to Saisei need to have a download speed set for them." if !$svc->{Hash}->{speed_down};
625   $self->{'__saisei_error'} = "There is no upload speed set for the service !--service,".$svc->{Hash}->{svcnum}.",".$rateplan."--! with host (".$svc->{Hash}->{ip_addr}."). All services that are to be exported to Saisei need to have a upload speed set for them." if !$svc->{Hash}->{speed_up};
626
627   my $new_rateplan = $self->api_call(
628       "PUT", 
629       "/rate_plans/$rateplan",
630       {
631         'downstream_rate' => $svc->{Hash}->{speed_down},
632         'upstream_rate' => $svc->{Hash}->{speed_up},
633       },
634   ) unless $self->{'__saisei_error'};
635
636   $self->{'__saisei_error'} = "Saisei could not create the rate plan $rateplan."
637     unless ($new_rateplan || $self->{'__saisei_error'});
638
639   return $new_rateplan;
640
641 }
642
643 =head2 api_modify_rateplan
644
645 Modify a new rateplan.
646
647 =cut
648
649 sub api_modify_rateplan {
650   my ($self,$svc,$rateplan_name) = @_;
651
652   # get policy list
653   my $policies = $self->api_get_policies();
654
655   foreach my $policy (@$policies) {
656     my $policyname = $policy->{name};
657     my $rate_multiplier = '';
658     if ($policy->{background}) { $rate_multiplier = ".01"; }
659     my $modified_rateplan = $self->api_call(
660       "PUT", 
661       "/rate_plans/$rateplan_name/partitions/$policyname",
662       {
663         'restricted'      =>  $policy->{assured},         # policy_assured_flag
664         'rate_multiplier' => $rate_multiplier,           # policy_background 0.1
665         'rate'            =>  $policy->{percent_rate}, # policy_percent_rate
666       },
667     );
668
669     $self->{'__saisei_error'} = "Saisei could not modify the rate plan $rateplan_name after it was created."
670       unless ($modified_rateplan || $self->{'__saisei_error'}); # should never happen
671     
672   }
673
674   return;
675  
676 }
677
678 =head2 api_modify_existing_rateplan
679
680 Modify a existing rateplan.
681
682 =cut
683
684 sub api_modify_existing_rateplan {
685   my ($self,$svc,$rateplan_name) = @_;
686
687   $self->{'__saisei_error'} = "There is no download speed set for the service !--service,".$svc->{Hash}->{svcnum}.",".$rateplan_name."--! with host (".$svc->{Hash}->{ip_addr}."). All services that are to be exported to Saisei need to have a download speed set for them." if !$svc->{Hash}->{speed_down};
688   $self->{'__saisei_error'} = "There is no upload speed set for the service !--service,".$svc->{Hash}->{svcnum}.",".$rateplan_name."--! with host (".$svc->{Hash}->{ip_addr}."). All services that are to be exported to Saisei need to have a upload speed set for them." if !$svc->{Hash}->{speed_up};
689
690   my $modified_rateplan = $self->api_call(
691     "PUT",
692     "/rate_plans/$rateplan_name",
693     {
694       'downstream_rate' => $svc->{Hash}->{speed_down},
695       'upstream_rate' => $svc->{Hash}->{speed_up},
696     },
697   );
698
699     $self->{'__saisei_error'} = "Saisei could not modify the rate plan $rateplan_name."
700       unless ($modified_rateplan || $self->{'__saisei_error'}); # should never happen
701
702   return;
703
704 }
705
706 =head2 api_create_user
707
708 Creates a user.
709
710 =cut
711
712 sub api_create_user {
713   my ($self,$user, $description, $location) = @_;
714
715   my $user_hash = {
716     'description' => $description,
717   };
718   $user_hash->{'map_location'} = $location if $location;
719
720   my $new_user = $self->api_call(
721       "PUT",
722       "/users/$user",
723       $user_hash,
724   );
725
726   $self->{'__saisei_error'} = "Saisei could not create the user $user"
727     unless ($new_user || $self->{'__saisei_error'}); # should never happen
728
729   return $new_user;
730
731 }
732
733 =head2 api_modify_user
734
735 Modify a user.
736
737 =cut
738
739 sub api_modify_user {
740   my ($self,$user, $description, $location) = @_;
741
742   my $user_hash = {
743     'description' => $description,
744   };
745   $user_hash->{'map_location'} = $location if $location;
746
747   my $modify_user = $self->api_call(
748       "PUT",
749       "/users/$user",
750       $user_hash,
751   );
752
753   $self->{'__saisei_error'} = "Saisei could not modify the user $user"
754     unless ($modify_user || $self->{'__saisei_error'}); # should never happen
755
756   return $modify_user;
757
758 }
759
760 =head2 api_create_accesspoint
761
762 Creates a access point.
763
764 =cut
765
766 sub api_create_accesspoint {
767   my ($self,$accesspoint, $upratelimit, $downratelimit, $location) = @_;
768
769   my $ap_hash = {
770     'downstream_rate_limit' => $downratelimit,
771     'upstream_rate_limit'   => $upratelimit,
772     'interface'             => $self->option('interface'),
773   };
774   $ap_hash->{'map_location'} = $location if $location;
775
776   my $new_accesspoint = $self->api_call(
777       "PUT",
778       "/access_points/$accesspoint",
779       $ap_hash,
780   );
781
782   $self->{'__saisei_error'} = "Saisei could not create the access point $accesspoint"
783     unless ($new_accesspoint || $self->{'__saisei_error'}); # should never happen
784   return;
785
786 }
787
788 =head2 api_modify_accesspoint
789
790 Modify a new access point.
791
792 =cut
793
794 sub api_modify_accesspoint {
795   my ($self, $accesspoint, $uplink, $location) = @_;
796
797   my $ap_hash = {
798     'uplink'    => $uplink,
799     'interface' => $self->option('interface'),
800   };
801   $ap_hash->{'map_location'} = $location if $location;
802
803   my $modified_accesspoint = $self->api_call(
804     "PUT",
805     "/access_points/$accesspoint",
806     $ap_hash,
807   );
808
809   $self->{'__saisei_error'} = "Saisei could not modify the access point $accesspoint after it was created."
810     unless ($modified_accesspoint || $self->{'__saisei_error'}); # should never happen
811
812   return;
813
814 }
815
816 =head2 api_modify_existing_accesspoint
817
818 Modify a existing accesspoint.
819
820 =cut
821
822 sub api_modify_existing_accesspoint {
823   my ($self, $accesspoint, $uplink, $upratelimit, $downratelimit, $location) = @_;
824
825   my $ap_hash = {
826     'downstream_rate_limit' => $downratelimit,
827     'upstream_rate_limit'   => $upratelimit,
828     'interface'             => $self->option('interface'),
829 #   'uplink'                => $uplink, # name of attached access point
830   };
831   $ap_hash->{'map_location'} = $location if $location;
832
833   my $modified_accesspoint = $self->api_call(
834     "PUT",
835     "/access_points/$accesspoint",
836     $ap_hash,
837   );
838
839   $self->{'__saisei_error'} = "Saisei could not modify the access point $accesspoint."
840     unless ($modified_accesspoint || $self->{'__saisei_error'}); # should never happen
841
842   return;
843
844 }
845
846 =head2 api_add_host_to_user
847
848 ties host to user, rateplan and default access point.
849
850 =cut
851
852 sub api_add_host_to_user {
853 #  my ($self,$user, $rateplan, $ip, $accesspoint, $location) = @_;
854   my ($self,$opt) = @_;
855   my $ip = $opt->{'ip'};
856   my $location = $opt->{'location'};
857
858   my $newhost_hash = {
859     'user'         => $opt->{'user'},
860     'rate_plan'    => $opt->{'rateplan'},
861     'access_point' => $opt->{'accesspoint'},
862   };
863   $newhost_hash->{'map_location'} = $location if $location;
864
865   my $new_host = $self->api_call(
866       "PUT", 
867       "/hosts/$ip",
868       $newhost_hash,
869   );
870
871   $self->{'__saisei_error'} = "Saisei could not create the host $ip"
872     unless ($new_host || $self->{'__saisei_error'}); # should never happen
873
874   return $new_host;
875
876 }
877
878 =head2 api_delete_host_to_user
879
880 unties host from user and rateplan.
881 this will set the host entry at Saisei to the default rate plan with the user and access point set to <none>.
882
883 =cut
884
885 sub api_delete_host_to_user {
886   my ($self,$user, $rateplan, $ip) = @_;
887
888   my $default_rate_plan = $self->api_call("GET", '?token=1&select=default_rate_plan');
889     return if $self->api_error;
890   $self->{'__saisei_error'} = "Can not delete the host as Saisei did not return a default rate plan. Please make sure Saisei has a default rateplan setup."
891     unless $default_rate_plan;
892
893   my $default_rateplan_name = $default_rate_plan->{collection}->[0]->{default_rate_plan}->{link}->{name};
894
895   my $delete_host = $self->api_call(
896       "PUT",
897       "/hosts/$ip",
898       {
899         'user'          => '<none>',
900         'access_point'  => '<none>',
901         'rate_plan'     => $default_rateplan_name,
902       },
903   );
904
905   $self->{'__saisei_error'} = "Saisei could not delete the host $ip"
906     unless ($delete_host || $self->{'__saisei_error'}); # should never happen
907
908   return $delete_host;
909
910 }
911
912 sub process_tower {
913   my ($self, $opt) = @_;
914
915   if (!$opt->{tower_uprate_limit} || !$opt->{tower_downrate_limit}) {
916     $self->{'__saisei_error'} = "Could not export tower !--tower,".$opt->{tower_num}.",".$opt->{tower_name}."--! because there was no up or down rates attached to the tower.  Saisei requires a up and down rate be attached to each tower.";
917     return { error => $self->api_error, };
918   }
919
920   my $existing_tower_ap;
921   my $tower_name = $opt->{tower_name};
922   my $location = $opt->{location};
923
924   #check if tower has been set up as an access point.
925   $existing_tower_ap = $self->api_get_accesspoint($tower_name) unless $self->{'__saisei_error'};
926
927   # modify the existing accesspoint if changing tower .
928   $self->api_modify_existing_accesspoint (
929     $tower_name,
930     '', # tower does not have a uplink on sectors.
931     $opt->{tower_uprate_limit},
932     $opt->{tower_downrate_limit},
933     $location,
934   ) if $existing_tower_ap->{collection} && $opt->{modify_existing};
935
936   #if tower does not exist as an access point create it.
937   $self->api_create_accesspoint(
938       $tower_name,
939       $opt->{tower_uprate_limit},
940       $opt->{tower_downrate_limit},
941       $location,
942   ) unless $existing_tower_ap->{collection};
943
944   my $accesspoint = $self->api_get_accesspoint($tower_name);
945
946   return { error => $self->api_error, } if $self->api_error;
947   return $accesspoint;
948 }
949
950 sub process_sector {
951   my ($self, $opt) = @_;
952
953   if (!$opt->{sector_name} || $opt->{sector_name} eq '_default') {
954     $self->{'__saisei_error'} = "No sector attached to Tower (".$opt->{tower_name}.") for service ".$opt->{'rateplan'}.".  Saisei requires a tower sector to be attached to each service that is exported to Saisei.";
955     return { error => $self->api_error, };
956   }
957
958   if (!$opt->{sector_uprate_limit} || !$opt->{sector_downrate_limit}) {
959     $self->{'__saisei_error'} = "Could not export sector !--tower,".$opt->{tower_num}.",".$opt->{sector_name}."--! because there was no up or down rates attached to the sector.  Saisei requires a up and down rate be attached to each sector.";
960     return { error => $self->api_error, };
961   }
962
963   my $existing_sector_ap;
964   my $sector_name = $opt->{sector_name};
965   my $location = $opt->{location};
966
967   #check if sector has been set up as an access point.
968   $existing_sector_ap = $self->api_get_accesspoint($sector_name);
969
970   # modify the existing accesspoint if changing sector .
971   $self->api_modify_existing_accesspoint (
972     $sector_name,
973     $opt->{tower_name},
974     $opt->{sector_uprate_limit},
975     $opt->{sector_downrate_limit},
976     $location,
977   ) if $existing_sector_ap && $opt->{modify_existing};
978
979   #if sector does not exist as an access point create it.
980   $self->api_create_accesspoint(
981     $sector_name,
982     $opt->{sector_uprate_limit},
983     $opt->{sector_downrate_limit},
984     $location,
985   ) unless $existing_sector_ap;
986
987   # Attach newly created sector to it's tower.
988   $self->api_modify_accesspoint($sector_name, $opt->{tower_name}, $location) unless ($self->{'__saisei_error'} || $existing_sector_ap);
989
990   # set access point to existing one or newly created one.
991   my $accesspoint = $existing_sector_ap ? $existing_sector_ap : $self->api_get_accesspoint($sector_name);
992
993   return { error => $self->api_error, } if $self->api_error;
994   return $accesspoint;
995 }
996
997 =head2 get_svc_location
998
999 sets location to lat and long from service, if no service location gets it from package, if still no location returns null.
1000
1001 =cut
1002
1003 sub get_svc_location {
1004   my ($self, $svc) = @_;
1005
1006   my $svc_location = '';
1007   $svc_location = $svc->{Hash}->{latitude}.','.$svc->{Hash}->{longitude} if ($svc->{Hash}->{latitude} && $svc->{Hash}->{longitude});
1008
1009   if (!$svc_location) {
1010     my $pkg_location = FS::Record::qsearchs({
1011       'table'   => 'cust_pkg',
1012       'addl_from' => 'LEFT JOIN cust_location USING (locationnum)',
1013       'hashref' => { 'pkgnum' => $svc->{Hash}->{pkgnum} },
1014     });
1015     $svc_location = $pkg_location->{Hash}->{latitude}.','.$pkg_location->{Hash}->{longitude} if ($pkg_location->{Hash}->{latitude} && $pkg_location->{Hash}->{longitude});
1016   }
1017
1018   return $svc_location;
1019 }
1020
1021 =head2 require_tower_and_sector
1022
1023 sets whether the service export requires a sector with it's tower.
1024
1025 =cut
1026
1027 sub require_tower_and_sector {
1028   1;
1029 }
1030
1031 =head2 tower_sector_required_fields
1032
1033 required fields needed for tower and sector export.
1034
1035 =cut
1036
1037 sub tower_sector_required_fields {
1038   my $fields = {
1039     'tower' => {
1040       'up_rate_limit'   => '1',
1041       'down_rate_limit' => '1',
1042     },
1043     'sector' => {
1044       'up_rate_limit'   => '1',
1045       'down_rate_limit' => '1',
1046       'ip_addr'         => '1',
1047     },
1048   };
1049   return $fields;
1050 }
1051
1052 sub required_fields {
1053   my @fields = ('svc_broadband__ip_addr_required', 'svc_broadband__speed_up_required', 'svc_broadband__speed_down_required', 'svc_broadband__sectornum_required');
1054   return @fields;
1055 }
1056
1057 sub process_virtual_ap {
1058   my ($self, $opt) = @_;
1059
1060   my $existing_virtual_ap;
1061   my $virtual_name = $opt->{virtual_name};
1062
1063   #check if virtual_ap has been set up as an access point.
1064   $existing_virtual_ap = $self->api_get_accesspoint($virtual_name);
1065
1066   # modify the existing virtual accesspoint if changing it. this should never happen
1067   $self->api_modify_existing_accesspoint (
1068     $virtual_name,
1069     $opt->{sector_name},
1070     $opt->{virtual_uprate_limit},
1071     $opt->{virtual_downrate_limit},
1072     $opt->{location},
1073   ) if $existing_virtual_ap && $opt->{modify_existing};
1074
1075   #if virtual ap does not exist as an access point create it.
1076   $self->api_create_accesspoint(
1077     $virtual_name,
1078     $opt->{virtual_uprate_limit},
1079     $opt->{virtual_downrate_limit},
1080     $opt->{location},
1081   ) unless $existing_virtual_ap;
1082
1083   my $update_sector;
1084   if ($existing_virtual_ap && (ref $existing_virtual_ap->{collection}->[0]->{uplink} eq "HASH") && ($existing_virtual_ap->{collection}->[0]->{uplink}->{link}->{name} ne $opt->{sector_name})) {
1085     $update_sector = 1;
1086   }
1087
1088   # Attach newly created virtual ap to tower sector ap or if sector has changed.
1089   $self->api_modify_accesspoint($virtual_name, $opt->{sector_name}, $opt->{location}) unless ($self->{'__saisei_error'} || ($existing_virtual_ap && !$update_sector));
1090
1091   # set access point to existing one or newly created one.
1092   my $accesspoint = $existing_virtual_ap ? $existing_virtual_ap : $self->api_get_accesspoint($virtual_name);
1093
1094   return $accesspoint;
1095 }
1096
1097 sub export_provisioned_services {
1098   my $job = shift;
1099   my $param = shift;
1100   my $force_update = shift;
1101
1102   my $part_export = FS::Record::qsearchs('part_export', { 'exportnum' => $param->{export_provisioned_services_exportnum}, } )
1103   or die "You are trying to use an unknown exportnum $param->{export_provisioned_services_exportnum}.  This export does not exist.\n";
1104   bless $part_export;
1105
1106   my @svcparts = FS::Record::qsearch({
1107     'table' => 'export_svc',
1108     'addl_from' => 'LEFT JOIN part_svc USING ( svcpart  ) ',
1109     'hashref'   => { 'exportnum' => $param->{export_provisioned_services_exportnum}, },
1110   });
1111   my $part_count = scalar @svcparts;
1112
1113   my $parts = join "', '", map { $_->{Hash}->{svcpart} } @svcparts;
1114
1115   my @svcs = FS::Record::qsearch({
1116     'table' => 'cust_svc',
1117     'addl_from' => 'LEFT JOIN svc_broadband USING ( svcnum  ) ',
1118     'extra_sql' => " WHERE svcpart in ('".$parts."')",
1119   }) unless !$parts;
1120
1121   my $svc_count = scalar @svcs;
1122
1123   my %status = {};
1124   for (my $c=1; $c <=100; $c=$c+1) { $status{int($svc_count * ($c/100))} = $c; }
1125
1126   my $process_count=0;
1127   foreach my $svc (@svcs) {
1128     if ($status{$process_count}) { my $s = $status{$process_count}; $job->update_statustext($s); }
1129     ## check if service exists as host if not export it.
1130     my $host = api_get_host($part_export, $svc->{Hash}->{ip_addr});
1131     die ("Please double check your credentials as ".$host->{message}."\n") if $host->{message};
1132     warn "Exporting service ".$svc->{Hash}->{ip_addr}."\n" if ($part_export->option('debug'));
1133     my $export_error;
1134     if ($force_update) { $export_error = _export_insert($part_export,$svc,$force_update); }
1135     else { $export_error = _export_insert($part_export,$svc) unless $host->{collection}; }
1136     if ($export_error) {
1137       warn "Error exporting service ".$svc->{Hash}->{ip_addr}."\n" if ($part_export->option('debug'));
1138       die ("$export_error\n");
1139     }
1140     $process_count++;
1141   }
1142
1143   return;
1144
1145 }
1146
1147 sub export_all_towers_sectors {
1148   my $job = shift;
1149   my $param = shift;
1150
1151   my $part_export = FS::Record::qsearchs('part_export', { 'exportnum' => $param->{export_provisioned_services_exportnum}, } )
1152   or die "You are trying to use an unknown exportnum $param->{export_provisioned_services_exportnum}.  This export does not exist.\n";
1153   bless $part_export;
1154
1155   my @towers = FS::Record::qsearch({
1156     'table' => 'tower',
1157   });
1158   my $tower_count = scalar @towers;
1159
1160   my %status = {};
1161   for (my $c=1; $c <=100; $c=$c+1) { $status{int($tower_count * ($c/100))} = $c; }
1162
1163   my $process_count=0;
1164   foreach my $tower (@towers) {
1165     if ($status{$process_count}) { my $s = $status{$process_count}; $job->update_statustext($s); }
1166     my $export_error = export_tower_sector($part_export,$tower);
1167     if ($export_error->{'error'}) {
1168       warn "Error exporting tower/sector (".$tower->{Hash}->{towername}.")\n" if ($part_export->option('debug'));
1169       die ($export_error->{'error'}."\n");
1170     }
1171     $process_count++;
1172   }
1173
1174   return;
1175
1176 }
1177
1178 sub force_export_all_virtual_ap {
1179   my $job = shift;
1180   my $param = shift;
1181   my $force_update = { 'update_virtual_ap' => '1', };
1182
1183   export_provisioned_services($job,$param,$force_update);
1184
1185   return;
1186 }
1187
1188 sub force_export_all_users {
1189   my $job = shift;
1190   my $param = shift;
1191
1192   my $part_export = FS::Record::qsearchs('part_export', { 'exportnum' => $param->{export_provisioned_services_exportnum}, } )
1193   or die "You are trying to use an unknown exportnum $param->{export_provisioned_services_exportnum}.  This export does not exist.\n";
1194   bless $part_export;
1195
1196   my @svcparts = FS::Record::qsearch({
1197     'table' => 'export_svc',
1198     'addl_from' => 'LEFT JOIN part_svc USING ( svcpart  ) ',
1199     'hashref'   => { 'exportnum' => $param->{export_provisioned_services_exportnum}, },
1200   });
1201   my $part_count = scalar @svcparts;
1202
1203   my $parts = join "', '", map { $_->{Hash}->{svcpart} } @svcparts;
1204
1205   my @svcs = FS::Record::qsearch({
1206     'table' => 'cust_svc',
1207     'addl_from' => 'LEFT JOIN svc_broadband USING ( svcnum  ) ',
1208     'extra_sql' => " WHERE svcpart in ('".$parts."')",
1209   }) unless !$parts;
1210
1211   my $svc_count = scalar @svcs;
1212
1213   my %status = {};
1214   for (my $c=1; $c <=100; $c=$c+1) { $status{int($svc_count * ($c/100))} = $c; }
1215
1216   my $process_count=0;
1217   foreach my $svc (@svcs) {
1218     my $description = $svc->{Hash}->{description};
1219     my $user = $svc->{Hash}->{svcnum};
1220     my $svc_location = get_svc_location($job, $svc);
1221     if ($status{$process_count}) { my $s = $status{$process_count}; $job->update_statustext($s); }
1222     warn "Exporting user ".$svc->{Hash}->{ip_addr}."\n" if ($part_export->option('debug'));
1223     my $export_error = export_user($part_export,$user,$description, $svc_location);
1224     if ($export_error) {
1225       warn "Error exporting user ".$svc->{Hash}->{svcnum}."\n" if ($part_export->option('debug'));
1226       die ($export_error->{'error'}."\n");
1227     }
1228     $process_count++;
1229   }
1230
1231   return;
1232
1233 }
1234
1235 sub test_export_report {
1236   my ($self, $opts) = @_;
1237   my @export_error;
1238
1239   ##  check all part services for export errors
1240   my @exports = FS::Record::qsearch('part_export', { 'exporttype' => "saisei", } );
1241   my $export_nums = join "', '", map { $_->{Hash}->{exportnum} } @exports;
1242
1243   my $svc_part_export_error;
1244   my @svcparts = FS::Record::qsearch({
1245     'table' => 'part_svc',
1246     'addl_from' => 'LEFT JOIN export_svc USING ( svcpart  ) ',
1247     'extra_sql' => " WHERE export_svc.exportnum in ('".$export_nums."')",
1248   });
1249   my $part_count = scalar @svcparts;
1250
1251   my $svc_part_error;
1252   foreach (@svcparts) {
1253     my $part_error->{'description'} = $_->svc;
1254     $part_error->{'link'} = $opts->{'fsurl'}."/edit/part_svc.cgi?".$_->svcpart;
1255
1256     foreach my $s ('speed_up', 'speed_down') {
1257       my $speed = $_->part_svc_column($s);
1258       if ($speed->columnflag eq "" || $speed->columnflag eq "D") {
1259         $part_error->{'errors'}->{$speed->columnname} = "Field ".$speed->columnname." is not set to be required and can be set while provisioning the service." unless $speed->required eq "Y";
1260       }
1261       elsif ($speed->columnflag eq "F" || $speed->columnflag eq "S") {
1262         $part_error->{'errors'}->{$speed->columnname} = "Field ".$speed->columnname." is set to auto fill while provisioning the service but there is no value set." unless $speed->columnvalue;
1263       }
1264       elsif ($speed->columnflag eq "P") {
1265         my $fcc_speed_name = "broadband_".$speed->columnvalue."stream";
1266         foreach my $part_pkg ( FS::Record::qsearchs({
1267                                  'table'   => 'part_pkg',
1268                                  'addl_from' => 'LEFT JOIN pkg_svc USING ( pkgpart  ) ',
1269                                  'extra_sql' => " WHERE pkg_svc.svcpart = ".$_->svcpart,
1270                               })) {
1271           my $pkglink = '<a href="'.$opts->{'fsurl'}.'/edit/part_pkg.cgi?'.$part_pkg->pkgpart.'"><FONT COLOR="red"><B>'.$part_pkg->pkg.'</B></FONT></a>';
1272           $part_error->{'errors'}->{$speed->columnname} = "Field ".$speed->columnname." is set to package FCC 477 information, but package ".$pkglink." does not have FCC ".$fcc_speed_name." set."
1273             unless $part_pkg->fcc_option($fcc_speed_name);
1274         }
1275       }
1276     }
1277     $part_error->{'errors'}->{'ip_addr'}    = "Field IP Address is not set to required" if $_->part_svc_column("ip_addr")->required ne "Y";
1278     $svc_part_error->{$_->svcpart} = $part_error if $part_error->{'errors'};
1279   }
1280
1281   $svc_part_export_error->{"services"}->{'description'} = "Service definitions";
1282   $svc_part_export_error->{"services"}->{'count'} = $part_count;
1283   $svc_part_export_error->{"services"}->{'errors'} = $svc_part_error if $svc_part_error;
1284
1285   push @export_error, $svc_part_export_error;
1286
1287   ##  check all provisioned cust services for export errors
1288   my $parts = join "', '", map { $_->{Hash}->{svcpart} } @svcparts;
1289   my $cust_svc_export_error;
1290   my @svcs = FS::Record::qsearch({
1291     'table' => 'cust_svc',
1292     'addl_from' => 'LEFT JOIN svc_broadband USING ( svcnum  ) ',
1293     'extra_sql' => " WHERE svcpart in ('".$parts."')",
1294   }) unless !$parts;
1295   my $svc_count = scalar @svcs;
1296
1297   my $cust_svc_error;
1298   foreach (@svcs) {
1299     my $svc_error->{'description'} = $_->description;
1300     $svc_error->{'link'} = $opts->{'fsurl'}."/edit/svc_broadband.cgi?".$_->svcnum;
1301
1302     foreach my $s ('speed_up', 'speed_down', 'ip_addr') {
1303         $svc_error->{'errors'}->{$s} = "Field ".$s." is not set and is required for this service to be exported to Saisei." unless $_->$s;
1304     }
1305
1306     my $sector = FS::Record::qsearchs({
1307         'table' => 'tower_sector',
1308         'extra_sql' => " WHERE sectornum = ".$_->sectornum." AND sectorname != '_default'",
1309     }) if $_->sectornum;
1310     if (!$sector) {
1311       $svc_error->{'errors'}->{'sectornum'} = "No tower sector is set for this service. There needs to be a tower and sector set to be exported to Saisei.";
1312     }
1313     else {
1314       foreach my $s ('up_rate_limit', 'down_rate_limit') {
1315         $svc_error->{'errors'}->{'sectornum'} = "The sector ".$sector->description." does not have a ".$s." set. The sector needs a ".$s." set to be exported to Saisei."
1316           unless $sector->$s;
1317       }
1318     }
1319     $cust_svc_error->{$_->svcnum} = $svc_error if $svc_error->{'errors'};
1320   }
1321
1322   $cust_svc_export_error->{"provisioned_services"}->{'description'} = "Provisioned services";
1323   $cust_svc_export_error->{"provisioned_services"}->{'count'} = $svc_count;
1324   $cust_svc_export_error->{"provisioned_services"}->{'errors'} = $cust_svc_error if $cust_svc_error;
1325
1326   push @export_error, $cust_svc_export_error;
1327
1328
1329   ##  check all towers and sectors for export errors
1330   my $tower_sector_export_error;
1331   my @towers = FS::Record::qsearch({
1332     'table' => 'tower',
1333   });
1334   my $tower_count = scalar @towers;
1335
1336   my $towers_error;
1337   foreach (@towers) {
1338     my $tower_error->{'description'} = $_->towername;
1339     $tower_error->{'link'} = $opts->{'fsurl'}."/edit/tower.html?".$_->towernum;
1340
1341     foreach my $s ('up_rate_limit', 'down_rate_limit') {
1342         $tower_error->{'errors'}->{$s} = "Field ".$s." is not set for the tower, this is required for this tower to be exported to Saisei." unless $_->$s;
1343     }
1344
1345     my @sectors = FS::Record::qsearch({
1346         'table' => 'tower_sector',
1347         'extra_sql' => " WHERE towernum = ".$_->towernum." AND sectorname != '_default' AND (up_rate_limit IS NULL OR down_rate_limit IS NULL)",
1348     }) if $_->towernum;
1349     foreach my $sector (@sectors) {
1350       foreach my $s ('up_rate_limit', 'down_rate_limit') {
1351         $tower_error->{'errors'}->{'sector_'.$s} = "The sector ".$sector->description." does not have a ".$s." set. The sector needs a ".$s." set to be exported to Saisei."
1352           if !$sector->$s;
1353       }
1354     }
1355     $towers_error->{$_->towernum} = $tower_error if $tower_error->{'errors'};
1356   }
1357
1358   $tower_sector_export_error->{"tower_sector"}->{'description'} = "Tower / Sector";
1359   $tower_sector_export_error->{"tower_sector"}->{'count'} = $tower_count;
1360   $tower_sector_export_error->{"tower_sector"}->{'errors'} = $towers_error if $towers_error;
1361
1362   push @export_error, $tower_sector_export_error;
1363
1364   return [@export_error];
1365
1366 }
1367
1368 =head1 SEE ALSO
1369
1370 L<FS::part_export>
1371
1372 =cut
1373
1374 1;