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