RT# 83450 - fixed rateplan export
[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 Storable qw(thaw);
9 use MIME::Base64;
10 use REST::Client;
11 use Data::Dumper;
12 use FS::Conf;
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   my $fcc_477_speeds;
315   if ($svc_part->{Hash}->{svc_broadband__speed_down} eq "down" || $svc_part->{Hash}->{svc_broadband__speed_up} eq "up") {
316     for my $type (qw( down up )) {
317       my $speed_type = "broadband_".$type."stream";
318       foreach my $pkg_svc (FS::Record::qsearch({
319         'table'     => 'pkg_svc',
320         'select'    => 'pkg_svc.*, part_pkg_fcc_option.fccoptionname, part_pkg_fcc_option.optionvalue',
321         'addl_from' => ' LEFT JOIN part_pkg_fcc_option USING (pkgpart) ',
322         'extra_sql' => " WHERE pkg_svc.svcpart = ".$svc_part->{Hash}->{svcpart}." AND pkg_svc.quantity > 0 AND part_pkg_fcc_option.fccoptionname = '".$speed_type."'",
323       })) { $fcc_477_speeds->{
324         $pkg_svc->{Hash}->{pkgpart}}->{$speed_type} = $pkg_svc->{Hash}->{optionvalue} * 1000 unless !$pkg_svc->{Hash}->{optionvalue}; }
325     }
326   }
327   else {
328     $fcc_477_speeds->{1}->{broadband_downstream} = $svc_part->{Hash}->{"svc_broadband__speed_down"};
329     $fcc_477_speeds->{1}->{broadband_upstream} = $svc_part->{Hash}->{"svc_broadband__speed_up"};
330   }
331
332   foreach my $key (keys %$fcc_477_speeds) {
333
334     $svc_part->{Hash}->{speed_down} = $fcc_477_speeds->{$key}->{broadband_downstream};
335     $svc_part->{Hash}->{speed_up} = $fcc_477_speeds->{$key}->{broadband_upstream};
336     $svc_part->{Hash}->{svc_broadband__speed_down} = $fcc_477_speeds->{$key}->{broadband_downstream};
337     $svc_part->{Hash}->{svc_broadband__speed_up} = $fcc_477_speeds->{$key}->{broadband_upstream};
338
339     my $temp_svc = $svc_part->{Hash};
340     my $svc_broadband = {};
341     map { if ($_ =~ /^svc_broadband__(.*)$/) { $svc_broadband->{Hash}->{$1} = $temp_svc->{$_}; }  } keys %$temp_svc;
342
343     my $rateplan_name = $self->get_rateplan_name($svc_broadband, $svc_part->{Hash}->{svc});
344
345     # check for existing rate plan
346     my $existing_rateplan;
347     $existing_rateplan = $self->api_get_rateplan($rateplan_name) unless $self->{'__saisei_error'};
348
349     # Modify the existing rate plan with new service data.
350     $self->api_modify_existing_rateplan($svc_broadband, $rateplan_name) unless ($self->{'__saisei_error'} || !$existing_rateplan);
351
352     # if no existing rate plan create one and modify it.
353     $self->api_create_rateplan($svc_broadband, $rateplan_name) unless $existing_rateplan;
354     $self->api_modify_rateplan($svc_part, $rateplan_name) unless ($self->{'__saisei_error'} || $existing_rateplan);
355
356   }
357
358   return $self->api_error;
359
360 }
361
362 sub export_tower_sector {
363   my ($self, $tower) = @_;
364
365   my $tower_location;
366   $tower_location = $tower->{Hash}->{latitude}.','.$tower->{Hash}->{longitude} if ($tower->{Hash}->{latitude} && $tower->{Hash}->{longitude});
367
368   #modify tower or create it.
369   my $tower_name = $tower->{Hash}->{towername};
370   $tower_name =~ s/\s/_/g;
371
372   my $tower_opt = {
373     'tower_name'           => $tower_name,
374     'tower_num'            => $tower->{Hash}->{towernum},
375     'tower_uprate_limit'   => $tower->{Hash}->{up_rate_limit},
376     'tower_downrate_limit' => $tower->{Hash}->{down_rate_limit},
377     'modify_existing'      => '1', # modify an existing access point with this info
378   };
379   $tower_opt->{'location'} = $tower_location if $tower_location;
380
381   my $tower_access_point = process_tower($self, $tower_opt);
382     return $tower_access_point if $tower_access_point->{error};
383
384   #get list of all access points
385   my $hash_opt = {
386       'table'     => 'tower_sector',
387       'select'    => '*',
388       'hashref'   => { 'towernum' => $tower->{Hash}->{towernum}, },
389   };
390
391   #for each one modify or create it.
392   foreach my $tower_sector ( FS::Record::qsearch($hash_opt) ) {
393     next if $tower_sector->{Hash}->{sectorname} eq "_default";
394     my $sector_name = $tower_sector->{Hash}->{sectorname};
395     $sector_name =~ s/\s/_/g;
396     my $sector_opt = {
397       'tower_name'            => $tower_name,
398       'tower_num'             => $tower_sector->{Hash}->{towernum},
399       'sector_name'           => $sector_name,
400       'sector_uprate_limit'   => $tower_sector->{Hash}->{up_rate_limit},
401       'sector_downrate_limit' => $tower_sector->{Hash}->{down_rate_limit},
402       'modify_existing'       => '1', # modify an existing access point with this info
403     };
404     $sector_opt->{'location'} = $tower_location if $tower_location;
405
406     my $sector_access_point = process_sector($self, $sector_opt) unless ($sector_name eq "_default");
407       return $sector_access_point if $sector_access_point->{error};
408   }
409
410   return { error => $self->api_error, };
411 }
412
413 sub export_user {
414   my ($self, $username, $description, $location) = @_;
415
416   $self->api_create_user($username, $description, $location);
417
418   return $self->api_error if $self->{'__saisei_error'};
419
420   return '';
421 }
422
423 ## creates the rateplan name
424 sub get_rateplan_name {
425   my ($self, $svc_broadband, $svc_name) = @_;
426
427   my $service_part = FS::Record::qsearchs( 'part_svc', { 'svcpart' => $svc_broadband->{Hash}->{svcpart} } ) unless $svc_name;
428   my $service_name = $svc_name ? $svc_name : $service_part->{Hash}->{svc};
429
430   my $rateplan_name = $service_name . " " . $svc_broadband->{Hash}->{speed_down} . "-" . $svc_broadband->{Hash}->{speed_up};
431   $rateplan_name =~ s/\s/_/g; $rateplan_name =~ s/[^A-Za-z0-9\-_]//g;
432
433   return $rateplan_name;
434 }
435
436 =head1 Saisei API
437
438 These methods allow access to the Saisei API using the credentials
439 set in the export options.
440
441 =cut
442
443 =head2 api_call
444
445 Accepts I<$method>, I<$path>, I<$params> hashref and optional.
446 Places an api call to the specified path and method with the specified params.
447 Returns the decoded json object returned by the api call.
448 Returns empty on failure;  retrieve error messages using L</api_error>.
449
450 =cut
451
452 sub api_call {
453   my ($self,$method,$path,$params) = @_;
454
455   $self->{'__saisei_error'} = '';
456   my $auth_info = $self->option('username') . ':' . $self->option('password');
457   $params ||= {};
458
459   warn "Calling $method on http://"
460     .$self->{Hash}->{machine}.':'.$self->option('port')
461     ."/rest/top/configurations/running/$path\n" if $self->option('debug');
462
463   my $data = encode_json($params) if keys %{ $params };
464
465   my $client = REST::Client->new();
466   $client->addHeader("Authorization", "Basic ".encode_base64($auth_info));
467   $client->setHost('http://'.$self->{Hash}->{machine}.':'.$self->option('port'));
468   $client->$method('/rest/top/configurations/running'.$path, $data, { "Content-type" => 'application/json'});
469
470   warn "Saisei Response Code is ".$client->responseCode()."\n" if $self->option('debug');
471
472   my $result;
473
474   if ($client->responseCode() eq '200' || $client->responseCode() eq '201') {
475     eval { $result = decode_json($client->responseContent()) };
476     unless ($result) {
477       $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.";
478       warn "Saisei RC 201 Response Content is not json\n".$client->responseContent()."\n" if $self->option('debug');
479       return;
480     }
481   }
482   elsif ($client->responseCode() eq '404') {
483     eval { $result = decode_json($client->responseContent()) };
484     unless ($result) {
485       $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.";
486       warn "Saisei RC 404 Response Content is not json\n".$client->responseContent()."\n" if $self->option('debug');
487       return;
488     }
489     ## check if message is for empty hash.
490     my($does_not_exist) = $result->{message} =~ /'(.*)' does not exist$/;
491     $self->{'__saisei_error'} = "Saisei Error: ".$result->{message} unless $does_not_exist;
492     warn "Saisei Response Content is\n".$client->responseContent."\n" if ($self->option('debug') && !$does_not_exist);
493     return;
494   }
495   elsif ($client->responseCode() eq '500') {
496     $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();
497     warn "Saisei Response Content is\n".$client->responseContent."\n" if $self->option('debug');
498     return;
499   }
500   else {
501     $self->{'__saisei_error'} = "Received Bad response from server during $method $path $data, we received responce code: " . $client->responseCode() . " " . $client->responseContent;
502     warn "Saisei Response Content is\n".$client->responseContent."\n" if $self->option('debug');
503     return; 
504   }
505
506   return $result;
507   
508 }
509
510 =head2 api_error
511
512 Returns the error string set by L</Saisei API> methods,
513 or a blank string if most recent call produced no errors.
514
515 =cut
516
517 sub api_error {
518   my $self = shift;
519   return $self->{'__saisei_error'} || '';
520 }
521
522 =head2 api_get_policies
523
524 Gets a list of global policies.
525
526 =cut
527
528 sub api_get_policies {
529   my $self = shift;
530
531   my $get_policies = $self->api_call("GET", '/policies/?token=1&order=name&start=0&limit=20&select=name%2Cpercent_rate%2Cassured%2C');
532   return if $self->api_error;
533   $self->{'__saisei_error'} = "Did not receive any global policies from Saisei."
534     unless $get_policies;
535
536   return $get_policies->{collection};
537 }
538
539 =head2 api_get_rateplan
540
541 Gets rateplan info for specific rateplan.
542
543 =cut
544
545 sub api_get_rateplan {
546   my $self = shift;
547   my $rateplan = shift;
548
549   my $get_rateplan = $self->api_call("GET", "/rate_plans/$rateplan");
550   return if $self->api_error;
551
552   return $get_rateplan;
553 }
554
555 =head2 api_get_user
556
557 Gets user info for specific user.
558
559 =cut
560
561 sub api_get_user {
562   my $self = shift;
563   my $user = shift;
564
565   my $get_user = $self->api_call("GET", "/users/$user");
566   return if $self->api_error;
567
568   return $get_user;
569 }
570
571 =head2 api_get_accesspoint
572
573 Gets user info for specific access point.
574
575 =cut
576
577 sub api_get_accesspoint {
578   my $self = shift;
579   my $accesspoint = shift;
580
581   my $get_accesspoint = $self->api_call("GET", "/access_points/$accesspoint");
582   return if $self->api_error;
583
584   return $get_accesspoint;
585 }
586
587 =head2 api_get_host
588
589 Gets user info for specific host.
590
591 =cut
592
593 sub api_get_host {
594   my $self = shift;
595   my $ip = shift;
596
597   my $get_host = $self->api_call("GET", "/hosts/$ip");
598
599   return { message => $self->api_error, } if $self->api_error;
600
601   return $get_host;
602 }
603
604 =head2 api_create_rateplan
605
606 Creates a rateplan.
607
608 =cut
609
610 sub api_create_rateplan {
611   my ($self, $svc, $rateplan) = @_;
612
613   $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};
614   $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};
615
616   my $new_rateplan = $self->api_call(
617       "PUT", 
618       "/rate_plans/$rateplan",
619       {
620         'downstream_rate' => $svc->{Hash}->{speed_down},
621         'upstream_rate' => $svc->{Hash}->{speed_up},
622       },
623   ) unless $self->{'__saisei_error'};
624
625   $self->{'__saisei_error'} = "Saisei could not create the rate plan $rateplan."
626     unless ($new_rateplan || $self->{'__saisei_error'});
627
628   return $new_rateplan;
629
630 }
631
632 =head2 api_modify_rateplan
633
634 Modify a new rateplan.
635
636 =cut
637
638 sub api_modify_rateplan {
639   my ($self,$svc,$rateplan_name) = @_;
640
641   # get policy list
642   my $policies = $self->api_get_policies();
643
644   foreach my $policy (@$policies) {
645     my $policyname = $policy->{name};
646     my $rate_multiplier = '';
647     if ($policy->{background}) { $rate_multiplier = ".01"; }
648     my $modified_rateplan = $self->api_call(
649       "PUT", 
650       "/rate_plans/$rateplan_name/partitions/$policyname",
651       {
652         'restricted'      =>  $policy->{assured},         # policy_assured_flag
653         'rate_multiplier' => $rate_multiplier,           # policy_background 0.1
654         'rate'            =>  $policy->{percent_rate}, # policy_percent_rate
655       },
656     );
657
658     $self->{'__saisei_error'} = "Saisei could not modify the rate plan $rateplan_name after it was created."
659       unless ($modified_rateplan || $self->{'__saisei_error'}); # should never happen
660     
661   }
662
663   return;
664  
665 }
666
667 =head2 api_modify_existing_rateplan
668
669 Modify a existing rateplan.
670
671 =cut
672
673 sub api_modify_existing_rateplan {
674   my ($self,$svc,$rateplan_name) = @_;
675
676   $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};
677   $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};
678
679   my $modified_rateplan = $self->api_call(
680     "PUT",
681     "/rate_plans/$rateplan_name",
682     {
683       'downstream_rate' => $svc->{Hash}->{speed_down},
684       'upstream_rate' => $svc->{Hash}->{speed_up},
685     },
686   );
687
688     $self->{'__saisei_error'} = "Saisei could not modify the rate plan $rateplan_name."
689       unless ($modified_rateplan || $self->{'__saisei_error'}); # should never happen
690
691   return;
692
693 }
694
695 =head2 api_create_user
696
697 Creates a user.
698
699 =cut
700
701 sub api_create_user {
702   my ($self,$user, $description, $location) = @_;
703
704   my $user_hash = {
705     'description' => $description,
706   };
707   $user_hash->{'map_location'} = $location if $location;
708
709   my $new_user = $self->api_call(
710       "PUT",
711       "/users/$user",
712       $user_hash,
713   );
714
715   $self->{'__saisei_error'} = "Saisei could not create the user $user"
716     unless ($new_user || $self->{'__saisei_error'}); # should never happen
717
718   return $new_user;
719
720 }
721
722 =head2 api_modify_user
723
724 Modify a user.
725
726 =cut
727
728 sub api_modify_user {
729   my ($self,$user, $description, $location) = @_;
730
731   my $user_hash = {
732     'description' => $description,
733   };
734   $user_hash->{'map_location'} = $location if $location;
735
736   my $modify_user = $self->api_call(
737       "PUT",
738       "/users/$user",
739       $user_hash,
740   );
741
742   $self->{'__saisei_error'} = "Saisei could not modify the user $user"
743     unless ($modify_user || $self->{'__saisei_error'}); # should never happen
744
745   return $modify_user;
746
747 }
748
749 =head2 api_create_accesspoint
750
751 Creates a access point.
752
753 =cut
754
755 sub api_create_accesspoint {
756   my ($self,$accesspoint, $upratelimit, $downratelimit, $location) = @_;
757
758   my $ap_hash = {
759     'downstream_rate_limit' => $downratelimit,
760     'upstream_rate_limit'   => $upratelimit,
761     'interface'             => $self->option('interface'),
762   };
763   $ap_hash->{'map_location'} = $location if $location;
764
765   my $new_accesspoint = $self->api_call(
766       "PUT",
767       "/access_points/$accesspoint",
768       $ap_hash,
769   );
770
771   $self->{'__saisei_error'} = "Saisei could not create the access point $accesspoint"
772     unless ($new_accesspoint || $self->{'__saisei_error'}); # should never happen
773   return;
774
775 }
776
777 =head2 api_modify_accesspoint
778
779 Modify a new access point.
780
781 =cut
782
783 sub api_modify_accesspoint {
784   my ($self, $accesspoint, $uplink, $location) = @_;
785
786   my $ap_hash = {
787     'uplink'    => $uplink,
788     'interface' => $self->option('interface'),
789   };
790   $ap_hash->{'map_location'} = $location if $location;
791
792   my $modified_accesspoint = $self->api_call(
793     "PUT",
794     "/access_points/$accesspoint",
795     $ap_hash,
796   );
797
798   $self->{'__saisei_error'} = "Saisei could not modify the access point $accesspoint after it was created."
799     unless ($modified_accesspoint || $self->{'__saisei_error'}); # should never happen
800
801   return;
802
803 }
804
805 =head2 api_modify_existing_accesspoint
806
807 Modify a existing accesspoint.
808
809 =cut
810
811 sub api_modify_existing_accesspoint {
812   my ($self, $accesspoint, $uplink, $upratelimit, $downratelimit, $location) = @_;
813
814   my $ap_hash = {
815     'downstream_rate_limit' => $downratelimit,
816     'upstream_rate_limit'   => $upratelimit,
817     'interface'             => $self->option('interface'),
818 #   'uplink'                => $uplink, # name of attached access point
819   };
820   $ap_hash->{'map_location'} = $location if $location;
821
822   my $modified_accesspoint = $self->api_call(
823     "PUT",
824     "/access_points/$accesspoint",
825     $ap_hash,
826   );
827
828   $self->{'__saisei_error'} = "Saisei could not modify the access point $accesspoint."
829     unless ($modified_accesspoint || $self->{'__saisei_error'}); # should never happen
830
831   return;
832
833 }
834
835 =head2 api_add_host_to_user
836
837 ties host to user, rateplan and default access point.
838
839 =cut
840
841 sub api_add_host_to_user {
842 #  my ($self,$user, $rateplan, $ip, $accesspoint, $location) = @_;
843   my ($self,$opt) = @_;
844   my $ip = $opt->{'ip'};
845   my $location = $opt->{'location'};
846
847   my $newhost_hash = {
848     'user'         => $opt->{'user'},
849     'rate_plan'    => $opt->{'rateplan'},
850     'access_point' => $opt->{'accesspoint'},
851   };
852   $newhost_hash->{'map_location'} = $location if $location;
853
854   my $new_host = $self->api_call(
855       "PUT", 
856       "/hosts/$ip",
857       $newhost_hash,
858   );
859
860   $self->{'__saisei_error'} = "Saisei could not create the host $ip"
861     unless ($new_host || $self->{'__saisei_error'}); # should never happen
862
863   return $new_host;
864
865 }
866
867 =head2 api_delete_host_to_user
868
869 unties host from user and rateplan.
870 this will set the host entry at Saisei to the default rate plan with the user and access point set to <none>.
871
872 =cut
873
874 sub api_delete_host_to_user {
875   my ($self,$user, $rateplan, $ip) = @_;
876
877   my $default_rate_plan = $self->api_call("GET", '?token=1&select=default_rate_plan');
878     return if $self->api_error;
879   $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."
880     unless $default_rate_plan;
881
882   my $default_rateplan_name = $default_rate_plan->{collection}->[0]->{default_rate_plan}->{link}->{name};
883
884   my $delete_host = $self->api_call(
885       "PUT",
886       "/hosts/$ip",
887       {
888         'user'          => '<none>',
889         'access_point'  => '<none>',
890         'rate_plan'     => $default_rateplan_name,
891       },
892   );
893
894   $self->{'__saisei_error'} = "Saisei could not delete the host $ip"
895     unless ($delete_host || $self->{'__saisei_error'}); # should never happen
896
897   return $delete_host;
898
899 }
900
901 sub process_tower {
902   my ($self, $opt) = @_;
903
904   if (!$opt->{tower_uprate_limit} || !$opt->{tower_downrate_limit}) {
905     $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.";
906     return { error => $self->api_error, };
907   }
908
909   my $existing_tower_ap;
910   my $tower_name = $opt->{tower_name};
911   my $location = $opt->{location};
912
913   #check if tower has been set up as an access point.
914   $existing_tower_ap = $self->api_get_accesspoint($tower_name) unless $self->{'__saisei_error'};
915
916   # modify the existing accesspoint if changing tower .
917   $self->api_modify_existing_accesspoint (
918     $tower_name,
919     '', # tower does not have a uplink on sectors.
920     $opt->{tower_uprate_limit},
921     $opt->{tower_downrate_limit},
922     $location,
923   ) if $existing_tower_ap->{collection} && $opt->{modify_existing};
924
925   #if tower does not exist as an access point create it.
926   $self->api_create_accesspoint(
927       $tower_name,
928       $opt->{tower_uprate_limit},
929       $opt->{tower_downrate_limit},
930       $location,
931   ) unless $existing_tower_ap->{collection};
932
933   my $accesspoint = $self->api_get_accesspoint($tower_name);
934
935   return { error => $self->api_error, } if $self->api_error;
936   return $accesspoint;
937 }
938
939 sub process_sector {
940   my ($self, $opt) = @_;
941
942   if (!$opt->{sector_name} || $opt->{sector_name} eq '_default') {
943     $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.";
944     return { error => $self->api_error, };
945   }
946
947   if (!$opt->{sector_uprate_limit} || !$opt->{sector_downrate_limit}) {
948     $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.";
949     return { error => $self->api_error, };
950   }
951
952   my $existing_sector_ap;
953   my $sector_name = $opt->{sector_name};
954   my $location = $opt->{location};
955
956   #check if sector has been set up as an access point.
957   $existing_sector_ap = $self->api_get_accesspoint($sector_name);
958
959   # modify the existing accesspoint if changing sector .
960   $self->api_modify_existing_accesspoint (
961     $sector_name,
962     $opt->{tower_name},
963     $opt->{sector_uprate_limit},
964     $opt->{sector_downrate_limit},
965     $location,
966   ) if $existing_sector_ap && $opt->{modify_existing};
967
968   #if sector does not exist as an access point create it.
969   $self->api_create_accesspoint(
970     $sector_name,
971     $opt->{sector_uprate_limit},
972     $opt->{sector_downrate_limit},
973     $location,
974   ) unless $existing_sector_ap;
975
976   # Attach newly created sector to it's tower.
977   $self->api_modify_accesspoint($sector_name, $opt->{tower_name}, $location) unless ($self->{'__saisei_error'} || $existing_sector_ap);
978
979   # set access point to existing one or newly created one.
980   my $accesspoint = $existing_sector_ap ? $existing_sector_ap : $self->api_get_accesspoint($sector_name);
981
982   return { error => $self->api_error, } if $self->api_error;
983   return $accesspoint;
984 }
985
986 =head2 get_svc_location
987
988 sets location to lat and long from service, if no service location gets it from package, if still no location returns null.
989
990 =cut
991
992 sub get_svc_location {
993   my ($self, $svc) = @_;
994
995   my $svc_location = '';
996   $svc_location = $svc->{Hash}->{latitude}.','.$svc->{Hash}->{longitude} if ($svc->{Hash}->{latitude} && $svc->{Hash}->{longitude});
997
998   if (!$svc_location) {
999     my $pkg_location = FS::Record::qsearchs({
1000       'table'   => 'cust_pkg',
1001       'addl_from' => 'LEFT JOIN cust_location USING (locationnum)',
1002       'hashref' => { 'pkgnum' => $svc->{Hash}->{pkgnum} },
1003     });
1004     $svc_location = $pkg_location->{Hash}->{latitude}.','.$pkg_location->{Hash}->{longitude} if ($pkg_location->{Hash}->{latitude} && $pkg_location->{Hash}->{longitude});
1005   }
1006
1007   return $svc_location;
1008 }
1009
1010 =head2 require_tower_and_sector
1011
1012 sets whether the service export requires a sector with it's tower.
1013
1014 =cut
1015
1016 sub require_tower_and_sector {
1017   1;
1018 }
1019
1020 =head2 tower_sector_required_fields
1021
1022 required fields needed for tower and sector export.
1023
1024 =cut
1025
1026 sub tower_sector_required_fields {
1027   my $fields = {
1028     'tower' => {
1029       'up_rate_limit'   => '1',
1030       'down_rate_limit' => '1',
1031     },
1032     'sector' => {
1033       'up_rate_limit'   => '1',
1034       'down_rate_limit' => '1',
1035       'ip_addr'         => '1',
1036     },
1037   };
1038   return $fields;
1039 }
1040
1041 sub required_fields {
1042   my @fields = ('svc_broadband__ip_addr_required', 'svc_broadband__speed_up_required', 'svc_broadband__speed_down_required', 'svc_broadband__sectornum_required');
1043   return @fields;
1044 }
1045
1046 sub process_virtual_ap {
1047   my ($self, $opt) = @_;
1048
1049   my $existing_virtual_ap;
1050   my $virtual_name = $opt->{virtual_name};
1051
1052   #check if virtual_ap has been set up as an access point.
1053   $existing_virtual_ap = $self->api_get_accesspoint($virtual_name);
1054
1055   # modify the existing virtual accesspoint if changing it. this should never happen
1056   $self->api_modify_existing_accesspoint (
1057     $virtual_name,
1058     $opt->{sector_name},
1059     $opt->{virtual_uprate_limit},
1060     $opt->{virtual_downrate_limit},
1061     $opt->{location},
1062   ) if $existing_virtual_ap && $opt->{modify_existing};
1063
1064   #if virtual ap does not exist as an access point create it.
1065   $self->api_create_accesspoint(
1066     $virtual_name,
1067     $opt->{virtual_uprate_limit},
1068     $opt->{virtual_downrate_limit},
1069     $opt->{location},
1070   ) unless $existing_virtual_ap;
1071
1072   my $update_sector;
1073   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})) {
1074     $update_sector = 1;
1075   }
1076
1077   # Attach newly created virtual ap to tower sector ap or if sector has changed.
1078   $self->api_modify_accesspoint($virtual_name, $opt->{sector_name}, $opt->{location}) unless ($self->{'__saisei_error'} || ($existing_virtual_ap && !$update_sector));
1079
1080   # set access point to existing one or newly created one.
1081   my $accesspoint = $existing_virtual_ap ? $existing_virtual_ap : $self->api_get_accesspoint($virtual_name);
1082
1083   return $accesspoint;
1084 }
1085
1086 sub export_provisioned_services {
1087   my $job = shift;
1088   my $param = thaw(decode_base64(shift));
1089   my $force_update = shift;
1090
1091   my $part_export = FS::Record::qsearchs('part_export', { 'exportnum' => $param->{export_provisioned_services_exportnum}, } )
1092   or die "You are trying to use an unknown exportnum $param->{export_provisioned_services_exportnum}.  This export does not exist.\n";
1093   bless $part_export;
1094
1095   my @svcparts = FS::Record::qsearch({
1096     'table' => 'export_svc',
1097     'addl_from' => 'LEFT JOIN part_svc USING ( svcpart  ) ',
1098     'hashref'   => { 'exportnum' => $param->{export_provisioned_services_exportnum}, },
1099   });
1100   my $part_count = scalar @svcparts;
1101
1102   my $parts = join "', '", map { $_->{Hash}->{svcpart} } @svcparts;
1103
1104   my @svcs = FS::Record::qsearch({
1105     'table' => 'cust_svc',
1106     'addl_from' => 'LEFT JOIN svc_broadband USING ( svcnum  ) ',
1107     'extra_sql' => " WHERE svcpart in ('".$parts."')",
1108   }) unless !$parts;
1109
1110   my $svc_count = scalar @svcs;
1111
1112   my %status = {};
1113   for (my $c=1; $c <=100; $c=$c+1) { $status{int($svc_count * ($c/100))} = $c; }
1114
1115   my $process_count=0;
1116   foreach my $svc (@svcs) {
1117     if ($status{$process_count}) { my $s = $status{$process_count}; $job->update_statustext($s); }
1118     ## check if service exists as host if not export it.
1119     my $host = api_get_host($part_export, $svc->{Hash}->{ip_addr});
1120     die ("Please double check your credentials as ".$host->{message}."\n") if $host->{message};
1121     warn "Exporting service ".$svc->{Hash}->{ip_addr}."\n" if ($part_export->option('debug'));
1122     my $export_error;
1123     if ($force_update) { $export_error = _export_insert($part_export,$svc,$force_update); }
1124     else { $export_error = _export_insert($part_export,$svc) unless $host->{collection}; }
1125     if ($export_error) {
1126       warn "Error exporting service ".$svc->{Hash}->{ip_addr}."\n" if ($part_export->option('debug'));
1127       die ("$export_error\n");
1128     }
1129     $process_count++;
1130   }
1131
1132   return;
1133
1134 }
1135
1136 sub export_all_towers_sectors {
1137   my $job = shift;
1138   my $param = thaw(decode_base64(shift));
1139
1140   my $part_export = FS::Record::qsearchs('part_export', { 'exportnum' => $param->{export_provisioned_services_exportnum}, } )
1141   or die "You are trying to use an unknown exportnum $param->{export_provisioned_services_exportnum}.  This export does not exist.\n";
1142   bless $part_export;
1143
1144   my @towers = FS::Record::qsearch({
1145     'table' => 'tower',
1146   });
1147   my $tower_count = scalar @towers;
1148
1149   my %status = {};
1150   for (my $c=1; $c <=100; $c=$c+1) { $status{int($tower_count * ($c/100))} = $c; }
1151
1152   my $process_count=0;
1153   foreach my $tower (@towers) {
1154     if ($status{$process_count}) { my $s = $status{$process_count}; $job->update_statustext($s); }
1155     my $export_error = export_tower_sector($part_export,$tower);
1156     if ($export_error->{'error'}) {
1157       warn "Error exporting tower/sector (".$tower->{Hash}->{towername}.")\n" if ($part_export->option('debug'));
1158       die ($export_error->{'error'}."\n");
1159     }
1160     $process_count++;
1161   }
1162
1163   return;
1164
1165 }
1166
1167 sub force_export_all_virtual_ap {
1168   my $job = shift;
1169   my $param = shift;
1170   my $force_update = { 'update_virtual_ap' => '1', };
1171
1172   export_provisioned_services($job,$param,$force_update);
1173
1174   return;
1175 }
1176
1177 sub force_export_all_users {
1178   my $job = shift;
1179   my $param = shift;
1180
1181   my $part_export = FS::Record::qsearchs('part_export', { 'exportnum' => $param->{export_provisioned_services_exportnum}, } )
1182   or die "You are trying to use an unknown exportnum $param->{export_provisioned_services_exportnum}.  This export does not exist.\n";
1183   bless $part_export;
1184
1185   my @svcparts = FS::Record::qsearch({
1186     'table' => 'export_svc',
1187     'addl_from' => 'LEFT JOIN part_svc USING ( svcpart  ) ',
1188     'hashref'   => { 'exportnum' => $param->{export_provisioned_services_exportnum}, },
1189   });
1190   my $part_count = scalar @svcparts;
1191
1192   my $parts = join "', '", map { $_->{Hash}->{svcpart} } @svcparts;
1193
1194   my @svcs = FS::Record::qsearch({
1195     'table' => 'cust_svc',
1196     'addl_from' => 'LEFT JOIN svc_broadband USING ( svcnum  ) ',
1197     'extra_sql' => " WHERE svcpart in ('".$parts."')",
1198   }) unless !$parts;
1199
1200   my $svc_count = scalar @svcs;
1201
1202   my %status = {};
1203   for (my $c=1; $c <=100; $c=$c+1) { $status{int($svc_count * ($c/100))} = $c; }
1204
1205   my $process_count=0;
1206   foreach my $svc (@svcs) {
1207     my $description = $svc->{Hash}->{description};
1208     my $user = $svc->{Hash}->{svcnum};
1209     my $svc_location = get_svc_location($job, $svc);
1210     if ($status{$process_count}) { my $s = $status{$process_count}; $job->update_statustext($s); }
1211     warn "Exporting user ".$svc->{Hash}->{ip_addr}."\n" if ($part_export->option('debug'));
1212     my $export_error = export_user($part_export,$user,$description, $svc_location);
1213     if ($export_error) {
1214       warn "Error exporting user ".$svc->{Hash}->{svcnum}."\n" if ($part_export->option('debug'));
1215       die ($export_error->{'error'}."\n");
1216     }
1217     $process_count++;
1218   }
1219
1220   return;
1221
1222 }
1223
1224 sub test_export_report {
1225   my ($self, $opts) = @_;
1226   my @export_error;
1227
1228   ##  check all part services for export errors
1229   my @exports = FS::Record::qsearch('part_export', { 'exporttype' => "saisei", } );
1230   my $export_nums = join "', '", map { $_->{Hash}->{exportnum} } @exports;
1231
1232   my $svc_part_export_error;
1233   my @svcparts = FS::Record::qsearch({
1234     'table' => 'part_svc',
1235     'addl_from' => 'LEFT JOIN export_svc USING ( svcpart  ) ',
1236     'extra_sql' => " WHERE export_svc.exportnum in ('".$export_nums."')",
1237   });
1238   my $part_count = scalar @svcparts;
1239
1240   my $svc_part_error;
1241   foreach (@svcparts) {
1242     my $part_error->{'description'} = $_->svc;
1243     $part_error->{'link'} = $opts->{'fsurl'}."/edit/part_svc.cgi?".$_->svcpart;
1244
1245     foreach my $s ('speed_up', 'speed_down') {
1246       my $speed = $_->part_svc_column($s);
1247       if ($speed->columnflag eq "" || $speed->columnflag eq "D") {
1248         $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";
1249       }
1250       elsif ($speed->columnflag eq "F" || $speed->columnflag eq "S") {
1251         $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;
1252       }
1253       elsif ($speed->columnflag eq "P") {
1254         my $fcc_speed_name = "broadband_".$speed->columnvalue."stream";
1255         foreach my $part_pkg ( FS::Record::qsearchs({
1256                                  'table'   => 'part_pkg',
1257                                  'addl_from' => 'LEFT JOIN pkg_svc USING ( pkgpart  ) ',
1258                                  'extra_sql' => " WHERE pkg_svc.svcpart = ".$_->svcpart,
1259                               })) {
1260           my $pkglink = '<a href="'.$opts->{'fsurl'}.'/edit/part_pkg.cgi?'.$part_pkg->pkgpart.'"><FONT COLOR="red"><B>'.$part_pkg->pkg.'</B></FONT></a>';
1261           $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."
1262             unless $part_pkg->fcc_option($fcc_speed_name);
1263         }
1264       }
1265     }
1266     $part_error->{'errors'}->{'ip_addr'}    = "Field IP Address is not set to required" if $_->part_svc_column("ip_addr")->required ne "Y";
1267     $svc_part_error->{$_->svcpart} = $part_error if $part_error->{'errors'};
1268   }
1269
1270   $svc_part_export_error->{"services"}->{'description'} = "Service definitions";
1271   $svc_part_export_error->{"services"}->{'count'} = $part_count;
1272   $svc_part_export_error->{"services"}->{'errors'} = $svc_part_error if $svc_part_error;
1273
1274   push @export_error, $svc_part_export_error;
1275
1276   ##  check all provisioned cust services for export errors
1277   my $parts = join "', '", map { $_->{Hash}->{svcpart} } @svcparts;
1278   my $cust_svc_export_error;
1279   my @svcs = FS::Record::qsearch({
1280     'table' => 'cust_svc',
1281     'addl_from' => 'LEFT JOIN svc_broadband USING ( svcnum  ) ',
1282     'extra_sql' => " WHERE svcpart in ('".$parts."')",
1283   }) unless !$parts;
1284   my $svc_count = scalar @svcs;
1285
1286   my $cust_svc_error;
1287   foreach (@svcs) {
1288     my $svc_error->{'description'} = $_->description;
1289     $svc_error->{'link'} = $opts->{'fsurl'}."/edit/svc_broadband.cgi?".$_->svcnum;
1290
1291     foreach my $s ('speed_up', 'speed_down', 'ip_addr') {
1292         $svc_error->{'errors'}->{$s} = "Field ".$s." is not set and is required for this service to be exported to Saisei." unless $_->$s;
1293     }
1294
1295     my $sector = FS::Record::qsearchs({
1296         'table' => 'tower_sector',
1297         'extra_sql' => " WHERE sectornum = ".$_->sectornum." AND sectorname != '_default'",
1298     }) if $_->sectornum;
1299     if (!$sector) {
1300       $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.";
1301     }
1302     else {
1303       foreach my $s ('up_rate_limit', 'down_rate_limit') {
1304         $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."
1305           unless $sector->$s;
1306       }
1307     }
1308     $cust_svc_error->{$_->svcnum} = $svc_error if $svc_error->{'errors'};
1309   }
1310
1311   $cust_svc_export_error->{"provisioned_services"}->{'description'} = "Provisioned services";
1312   $cust_svc_export_error->{"provisioned_services"}->{'count'} = $svc_count;
1313   $cust_svc_export_error->{"provisioned_services"}->{'errors'} = $cust_svc_error if $cust_svc_error;
1314
1315   push @export_error, $cust_svc_export_error;
1316
1317
1318   ##  check all towers and sectors for export errors
1319   my $tower_sector_export_error;
1320   my @towers = FS::Record::qsearch({
1321     'table' => 'tower',
1322   });
1323   my $tower_count = scalar @towers;
1324
1325   my $towers_error;
1326   foreach (@towers) {
1327     my $tower_error->{'description'} = $_->towername;
1328     $tower_error->{'link'} = $opts->{'fsurl'}."/edit/tower.html?".$_->towernum;
1329
1330     foreach my $s ('up_rate_limit', 'down_rate_limit') {
1331         $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;
1332     }
1333
1334     my @sectors = FS::Record::qsearch({
1335         'table' => 'tower_sector',
1336         'extra_sql' => " WHERE towernum = ".$_->towernum." AND sectorname != '_default' AND (up_rate_limit IS NULL OR down_rate_limit IS NULL)",
1337     }) if $_->towernum;
1338     foreach my $sector (@sectors) {
1339       foreach my $s ('up_rate_limit', 'down_rate_limit') {
1340         $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."
1341           if !$sector->$s;
1342       }
1343     }
1344     $towers_error->{$_->towernum} = $tower_error if $tower_error->{'errors'};
1345   }
1346
1347   $tower_sector_export_error->{"tower_sector"}->{'description'} = "Tower / Sector";
1348   $tower_sector_export_error->{"tower_sector"}->{'count'} = $tower_count;
1349   $tower_sector_export_error->{"tower_sector"}->{'errors'} = $towers_error if $towers_error;
1350
1351   push @export_error, $tower_sector_export_error;
1352
1353   return [@export_error];
1354
1355 }
1356
1357 =head1 SEE ALSO
1358
1359 L<FS::part_export>
1360
1361 =cut
1362
1363 1;