RT# 83450 - fixed rateplan export
[freeside.git] / FS / FS / part_export / voip_innovations2.pm
1 package FS::part_export::voip_innovations2;
2
3 use vars qw(@ISA %info);
4 use Tie::IxHash;
5 use FS::Record qw(qsearch dbh);
6 use FS::part_export;
7 use FS::phone_avail;
8 use Data::Dumper;
9
10 @ISA = qw(FS::part_export);
11
12 tie my %options, 'Tie::IxHash',
13   'login'         => { label=>'VoIP Innovations API login' },
14   'password'      => { label=>'VoIP Innovations API password' },
15   'endpointgroup' => { label=>'VoIP Innovations endpoint group number' },
16   'e911'          => { label=>'Provision E911 data',
17                        type=>'checkbox',
18                      },
19   'no_provision_did' => { label=>'Disable DID provisioning',
20                           type=>'checkbox',
21                         },
22   'dry_run'       => { label=>"Test mode - don't actually provision",
23                        type=>'checkbox',
24                      },
25 ;
26
27 %info = (
28   'svc'     => 'svc_phone',
29   'desc'    => 'Provision phone numbers / E911 to VoIP Innovations (API 2.0)',
30   'options' => \%options,
31   'no_machine' => 1,
32   'notes'   => <<'END'
33 Requires installation of
34 <a href="http://search.cpan.org/dist/Net-VoIP_Innovations">Net::VoIP_Innovations</a>
35 from CPAN.
36 END
37 );
38
39 sub rebless { shift; }
40
41 sub can_get_dids {
42   my $self = shift;
43   ! $self->option('no_provision_did');
44 }
45
46 sub get_dids {
47   my $self = shift;
48   my %opt = ref($_[0]) ? %{$_[0]} : @_;
49
50   my %getdids = ();
51   #  'orderby' => 'npa', #but it doesn't seem to work :/
52
53   if ( $opt{'areacode'} && $opt{'exchange'} ) { #return numbers
54     %getdids = ( 'npa'   => $opt{'areacode'},
55                  'nxx'   => $opt{'exchange'},
56                );
57   } elsif ( $opt{'areacode'} ) { #return city (npa-nxx-XXXX)
58     %getdids = ( 'npa'   => $opt{'areacode'} );
59   } elsif ( $opt{'state'} ) {
60
61     my @avail = qsearch({
62       'table'    => 'phone_avail',
63       'hashref'  => { 'exportnum'   => $self->exportnum,
64                       'countrycode' => '1', #don't hardcode me when gp goes int'l
65                       'state'       => $opt{'state'},
66                     },
67       'order_by' => 'ORDER BY npa',
68     });
69
70     return [ map $_->npa, @avail ] if @avail; #return cached area codes instead
71
72     #otherwise, search for em
73     %getdids = ( 'state' => $opt{'state'} );
74
75   }
76
77   my $dids = $self->gp_command('getDIDs', %getdids);
78
79   if ( $dids->{'type'} eq 'Error' ) {
80     my $error =  "Error running VoIP Innovations getDIDs: ".
81         $dids->{'statuscode'}. ': '. $dids->{'status'}. "\n";
82     warn $error;
83     die $error;
84   }
85
86   my $search = $dids->{'search'};
87
88   if ( $search->{'statuscode'} == 302200 ) {
89     return [];
90   } elsif ( $search->{'statuscode'} != 100 ) {
91
92     my $error = "Error running VoIP Innovations getDIDs: ";
93     if ( $search->{'statuscode'} || $search->{'status'} ) {
94       $error .= $search->{'statuscode'}. ': '. $search->{'status'}. "\n";
95     } else {
96       $error .= Dumper($search);
97     }
98     warn $error;
99     die $error;
100   }
101
102   my @return = ();
103
104   #my $latas = $search->{state}{lata};
105   my %latas;
106   if ( grep $search->{state}{lata}{$_}, qw(name rate_center) ) {
107     %latas = map $search->{state}{lata}{$_},
108                  qw(name rate_center);
109   } else {
110     %latas = %{ $search->{state}{lata} };
111   } 
112
113   foreach my $lata ( keys %latas ) {
114
115     #warn "LATA $lata";
116     
117     #my $l = $latas{$lata};
118     #$l = $l->{rate_center} if exists $l->{rate_center};
119     
120     my $lata_dids = $self->gp_command('getDIDs', %getdids, 'lata'=>$lata);
121     my $lata_search = $lata_dids->{'search'};
122     unless ( $lata_search->{'statuscode'} == 100 ) {
123       die "Error running VoIP Innovations getDIDs: ". $lata_search->{'status'}; #die??
124     }
125    
126     my $l = $lata_search->{state}{lata}{'rate_center'};
127
128     #use Data::Dumper;
129     #warn Dumper($l);
130
131     my %rate_center;
132     if ( grep $l->{$_}, qw(name friendlyname) ) {
133       %rate_center = map $l->{$_},
134                          qw(name friendlyname);
135     } else {
136       %rate_center = %$l;
137     } 
138
139     foreach my $rate_center ( keys %rate_center ) {
140       
141       #warn "rate center $rate_center";
142
143       my $rc = $rate_center{$rate_center}; 
144       $rc = $rc->{friendlyname} if exists $rc->{friendlyname};
145
146       my @r = ();
147       if ( exists($rc->{npa}) ) {
148         @r = ($rc);
149       } else {
150         @r = map { { 'name'=>$_, %{ $rc->{$_} } }; } keys %$rc
151       }
152
153       foreach my $r (@r) {
154
155         my @npa = ();
156         if ( exists($r->{npa}{name}) ) {
157           @npa = ($r->{npa})
158         } else {
159           @npa = map { { 'name'=>$_, %{ $r->{npa}{$_} } } } keys %{ $r->{npa} };
160         }
161
162         foreach my $npa (@npa) {
163
164           if ( $opt{'areacode'} && $opt{'exchange'} ) { #return numbers
165
166             #warn Dumper($npa);
167
168             my $tn = $npa->{nxx}{tn} || $npa->{nxx}{$opt{'exchange'}}{tn};
169
170             my @tn = ref($tn) eq 'ARRAY' ? @$tn : ($tn);
171             #push @return, @tn;
172             push @return,
173               map {
174                     if ( /^\s*(\d{3})(\d{3})(\d{4})\s*$/ ) {
175                       "$1-$2-$3";
176                     } else {
177                       $_;
178                     }
179                   }
180                map { ref($_) eq 'HASH' ? $_->{'content'} : $_ } #tier always 2?
181                @tn;
182
183           } elsif ( $opt{'areacode'} ) { #return city (npa-nxx-XXXX)
184
185             if ( $npa->{nxx}{name} ) {
186               @nxx = ( $npa->{nxx}{name} );
187             } else {
188               @nxx = keys %{ $npa->{nxx} };
189             }
190
191             push @return, map { $r->{name}. ' ('. $npa->{name}. "-$_-XXXX)"; }
192                               @nxx;
193
194           } elsif ( $opt{'state'} ) { #and not other things, then return areacode
195             #my $ac = $npa->{name};
196             #use Data::Dumper;
197             #warn Dumper($r) unless length($ac) == 3;
198
199             push @return, $npa->{name}
200               unless grep { $_ eq $npa->{name} } @return;
201
202           } else {
203             warn "WARNING: returning nothing for get_dids without known options"; #?
204           }
205
206         } #foreach my $npa
207
208       } #foreach my $r
209
210     } #foreach my $rate_center
211
212   } #foreach my $lata
213
214   if ( $opt{'areacode'} && $opt{'exchange'} ) { #return numbers
215     @return = sort { $a cmp $b } @return; #string comparison actually dwiw
216   } elsif ( $opt{'areacode'} ) { #return city (npa-nxx-XXXX)
217     @return = sort { lc($a) cmp lc($b) } @return;
218   } elsif ( $opt{'state'} ) { #and not other things, then return areacode
219
220     #populate cache
221
222     local $SIG{HUP} = 'IGNORE';
223     local $SIG{INT} = 'IGNORE';
224     local $SIG{QUIT} = 'IGNORE';
225     local $SIG{TERM} = 'IGNORE';
226     local $SIG{TSTP} = 'IGNORE';
227     local $SIG{PIPE} = 'IGNORE';
228
229     my $oldAutoCommit = $FS::UID::AutoCommit;
230     local $FS::UID::AutoCommit = 0;
231     my $dbh = dbh;
232
233     my $errmsg = 'WARNING: error populating phone availability cache: ';
234     my $error = '';
235     foreach my $return (@return) {
236       my $phone_avail = new FS::phone_avail {
237         'exportnum'   => $self->exportnum,
238         'countrycode' => '1', #don't hardcode me when gp goes int'l
239         'state'       => $opt{'state'},
240         'npa'         => $return,
241       };
242       $error = $phone_avail->insert();
243       if ( $error ) {
244         warn $errmsg.$error;
245         last;
246       }
247     }
248
249     if ( $error ) {
250       $dbh->rollback if $oldAutoCommit;
251     } else {
252       $dbh->commit or warn $errmsg.$dbh->errstr if $oldAutoCommit;
253     }
254
255     #end populate cache
256
257     #@return = sort { (split(' ', $a))[0] <=> (split(' ', $b))[0] } @return;
258     @return = sort { $a <=> $b } @return;
259   } else {
260     warn "WARNING: returning nothing for get_dids without known options"; #?
261   }
262
263   \@return;
264
265 }
266
267 sub gp_command {
268   my( $self, $command, @args ) = @_;
269
270   eval "use Net::VoIP_Innovations 2.00;";
271   if ( $@ ) {
272     warn $@;
273     die $@;
274   }
275
276   my $gp = Net::VoIP_Innovations->new(
277     'login'    => $self->option('login'),
278     'password' => $self->option('password'),
279     #'debug'    => $debug,
280   );
281
282   $gp->$command(@args);
283 }
284
285
286 sub _export_insert {
287   my( $self, $svc_phone ) = (shift, shift);
288
289   return '' if $self->option('dry_run');
290
291   #we want to provision and catch errors now, not queue
292
293   unless ( $self->option('no_provision_did') ) {
294
295     ###
296     # reserveDID
297     ###
298
299     my $r = $self->gp_command('reserveDID',
300       'did'           => $svc_phone->phonenum,
301       'minutes'       => 1,
302       'endpointgroup' => $self->option('endpointgroup'),
303     );
304
305     my $rdid = $r->{did};
306
307     if ( $rdid->{'statuscode'} != 100 ) {
308       return "Error running VoIP Innovations reserveDID: ".
309              $rdid->{'statuscode'}. ': '. $rdid->{'status'};
310     }
311
312     ###
313     # assignDID
314     ###
315
316     my $a = $self->gp_command('assignDID',
317       'did'           => $svc_phone->phonenum,
318       'endpointgroup' => $self->option('endpointgroup'),
319       #'rewrite'
320       #'cnam'
321     );
322
323     my $adid = $a->{did};
324
325     if ( $adid->{'statuscode'} != 100 ) {
326       return "Error running VoIP Innovations assignDID: ".
327              $adid->{'statuscode'}. ': '. $adid->{'status'};
328     }
329
330   }
331
332   ###
333   # 911Insert
334   ###
335
336   if ( $self->option('e911') ) {
337
338     my %location_hash = $svc_phone->location_hash;
339     my( $zip, $plus4 ) = split('-', $location_hash->{zip});
340     my $e = $self->gp_command('911Insert',
341       'did'        => $svc_phone->phonenum,
342       'Address1'   => $location_hash{address1},
343       'Address2'   => $location_hash{address2},
344       'City'       => $location_hash{city},
345       'State'      => $location_hash{state},
346       'ZipCode'    => $zip,
347       'PlusFour'   => $plus4,
348       'CallerName' =>
349         $svc_phone->phone_name
350           || $svc_phone->cust_svc->cust_pkg->cust_main->contact_firstlast,
351     );
352
353     my $edid = $e->{did};
354
355     if ( $edid->{'statuscode'} != 100 ) {
356       return "Error running VoIP Innovations 911Insert: ".
357              $edid->{'statuscode'}. ': '. $edid->{'status'};
358     }
359
360   }
361
362   '';
363 }
364
365 sub _export_replace {
366   my( $self, $new, $old ) = (shift, shift, shift);
367
368   #hmm, anything to change besides E911 data?
369
370   ###
371   # 911Update
372   ###
373
374   if ( $self->option('e911') ) {
375
376     my %location_hash = $svc_phone->location_hash;
377     my( $zip, $plus4 ) = split('-', $location_hash->{zip});
378     my $e = $self->gp_command('911Update',
379       'did'        => $svc_phone->phonenum,
380       'Address1'   => $location_hash{address1},
381       'Address2'   => $location_hash{address2},
382       'City'       => $location_hash{city},
383       'State'      => $location_hash{state},
384       'ZipCode'    => $zip,
385       'PlusFour'   => $plus4,
386       'CallerName' =>
387         $svc_phone->phone_name
388           || $svc_phone->cust_svc->cust_pkg->cust_main->contact_firstlast,
389     );
390
391     my $edid = $e->{did};
392
393     if ( $edid->{'statuscode'} != 100 ) {
394       return "Error running VoIP Innovations 911Update: ".
395              $edid->{'statuscode'}. ': '. $edid->{'status'};
396     }
397
398   }
399
400   '';
401 }
402
403 sub _export_delete {
404   my( $self, $svc_phone ) = (shift, shift);
405
406   return '' if $self->option('dry_run');
407
408   #probably okay to queue the deletion...?
409   #but hell, let's do it inline anyway, who wants phone numbers hanging around
410
411   unless ( $self->option('no_provision_did') ) {
412
413     my $r = $self->gp_command('releaseDID',
414       'did'           => $svc_phone->phonenum,
415     );
416
417     my $rdid = $r->{did};
418
419     if ( $rdid->{'statuscode'} != 100 ) {
420       return "Error running VoIP Innovations releaseDID: ".
421              $rdid->{'statuscode'}. ': '. $rdid->{'status'};
422     }
423
424   }
425
426   #delete e911 information?  assuming release clears all that
427   #if ( $self->option('e911') ) {
428   #  # but need to handle the no_provision_did case
429   #}
430
431   '';
432 }
433
434 sub _export_suspend {
435   my( $self, $svc_phone ) = (shift, shift);
436   #nop for now
437   '';
438 }
439
440 sub _export_unsuspend {
441   my( $self, $svc_phone ) = (shift, shift);
442   #nop for now
443   '';
444 }
445
446 #hmm, might forgo queueing entirely for most things, data is too much of a pita
447 #sub globalpops_voip_queue {
448 #  my( $self, $svcnum, $method ) = (shift, shift, shift);
449 #  my $queue = new FS::queue {
450 #    'svcnum' => $svcnum,
451 #    'job'    => 'FS::part_export::globalpops_voip::globalpops_voip_command',
452 #  };
453 #  $queue->insert(
454 #    $self->option('login'),
455 #    $self->option('password'),
456 #    $method,
457 #    @_,
458 #  );
459 #}
460 #
461 #sub globalpops_voip_command {
462 #  my($login, $password, $method, @args) = @_;
463 #
464 #  eval "use Net::GlobalPOPs::MediaServicesAPI 0.03;";
465 #  die $@ if $@;
466 #
467 #  my $gp = new Net::GlobalPOPs::MediaServicesAPI
468 #                 'login'    => $login,
469 #                 'password' => $password,
470 #                 #'debug'    => 1,
471 #               ;
472 #
473 #  my $return = $gp->$method( @args );
474 #
475 #  #$return->{'status'} 
476 #  #$return->{'statuscode'} 
477 #
478 #  die $return->{'status'} if $return->{'statuscode'};
479 #
480 #}
481
482 1;
483