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