correctly parse error response from e911 provisioning, RT#76262
[freeside.git] / FS / FS / part_export / vitelity.pm
1 package FS::part_export::vitelity;
2 use base qw( FS::part_export );
3
4 use vars qw( %info );
5 use Tie::IxHash;
6 use FS::Record qw( qsearch dbh );
7 use FS::phone_avail;
8
9 tie my %options, 'Tie::IxHash',
10   'login'              => { label=>'Vitelity API login' },
11   'pass'               => { label=>'Vitelity API password' },
12   'routesip'           => { label=>'routesip (optional sub-account)' },
13   'type'               => { label=>'type (optional DID type to order)' },
14   'fax'                => { label=>'vfax service', type=>'checkbox' },
15   'restrict_selection' => { type    => 'select',
16                             label   => 'Restrict DID Selection', 
17                             options => [ '', 'tollfree', 'non-tollfree' ],
18                           },
19   'dry_run'            => { label => "Test mode - don't actually provision",
20                             type  => 'checkbox',
21                           },
22   'disable_e911'       => { label => "Disable E911 provisioning",
23                             type  => 'checkbox',
24                           },
25 ;
26
27 %info = (
28   'svc'        => 'svc_phone',
29   'desc'       => 'Provision phone numbers to Vitelity',
30   'options'    => \%options,
31   'no_machine' => 1,
32   'notes'      => <<'END'
33 Requires installation of
34 <a href="http://search.cpan.org/dist/Net-Vitelity">Net::Vitelity</a>
35 from CPAN.
36 <br><br>
37 routesip - optional Vitelity sub-account to which newly ordered DIDs will be routed
38 <br>type - optional DID type (perminute, unlimited, or your-pri)
39 END
40 );
41
42 sub rebless { shift; }
43
44 sub get_dids_can_tollfree { 1; };
45
46 sub get_dids {
47   my $self = shift;
48   my %opt = ref($_[0]) ? %{$_[0]} : @_;
49
50   if ( $opt{'tollfree'} ) {
51     my $command = 'listtollfree';
52     $command = 'listdids' if $self->option('fax');
53     my @tollfree = $self->vitelity_command($command);
54     my @ret = ();
55
56     return [] if ( $tollfree[0] eq 'noneavailable' || $tollfree[0] eq 'none');
57
58     foreach my $did ( @tollfree ) {
59         $did =~ /^(\d{3})(\d{3})(\d{4})/ or die "unparsable did $did\n";
60         push @ret, $did;
61     }
62
63     my @sorted_ret = sort @ret;
64     return \@sorted_ret;
65
66   } elsif ( $opt{'ratecenter'} && $opt{'state'} ) { 
67
68     my %flushopts = ( 'state' => $opt{'state'}, 
69                     'ratecenter' => $opt{'ratecenter'},
70                     'exportnum' => $self->exportnum
71                   );
72     FS::phone_avail::flush( \%flushopts );
73       
74     local $SIG{HUP} = 'IGNORE';
75     local $SIG{INT} = 'IGNORE';
76     local $SIG{QUIT} = 'IGNORE';
77     local $SIG{TERM} = 'IGNORE';
78     local $SIG{TSTP} = 'IGNORE';
79     local $SIG{PIPE} = 'IGNORE';
80
81     my $oldAutoCommit = $FS::UID::AutoCommit;
82     local $FS::UID::AutoCommit = 0;
83     my $dbh = dbh;
84
85     my $errmsg = 'WARNING: error populating phone availability cache: ';
86
87     my $command = 'listlocal';
88     $command = 'listdids' if $self->option('fax');
89     my @dids = $self->vitelity_command( $command,
90                                         'state'      => $opt{'state'},
91                                         'ratecenter' => $opt{'ratecenter'},
92                                       );
93     # XXX: Options: type=unlimited OR type=pri
94
95     next if ( $dids[0] eq 'unavailable'  || $dids[0] eq 'noneavailable' );
96     die "missingdata error running Vitelity API" if $dids[0] eq 'missingdata';
97
98     foreach my $did ( @dids ) {
99       $did =~ /^(\d{3})(\d{3})(\d{4})/ or die "unparsable did $did\n";
100       my($npa, $nxx, $station) = ($1, $2, $3);
101
102       my $phone_avail = new FS::phone_avail {
103           'exportnum'   => $self->exportnum,
104           'countrycode' => '1', # vitelity is US/CA only now
105           'state'       => $opt{'state'},
106           'npa'         => $npa,
107           'nxx'         => $nxx,
108           'station'     => $station,
109           'name'        => $opt{'ratecenter'},
110       };
111
112       my $error = $phone_avail->insert();
113       if ( $error ) {
114           $dbh->rollback if $oldAutoCommit;
115           die $errmsg.$error;
116       }
117
118     }
119     $dbh->commit or warn $errmsg.$dbh->errstr if $oldAutoCommit;
120
121     return [
122       map { join('-', $_->npa, $_->nxx, $_->station ) }
123           qsearch({
124             'table'    => 'phone_avail',
125             'hashref'  => { 'exportnum'   => $self->exportnum,
126                             'countrycode' => '1', # vitelity is US/CA only now
127                             'name'         => $opt{'ratecenter'},
128                             'state'          => $opt{'state'},
129                           },
130             'order_by' => 'ORDER BY npa, nxx, station',
131           })
132     ];
133
134   } elsif ( $opt{'areacode'} ) { 
135
136     my @rc = map { $_->{'Hash'}->{name}.", ".$_->state } 
137           qsearch({
138             'select'   => 'DISTINCT name, state',
139             'table'    => 'phone_avail',
140             'hashref'  => { 'exportnum'   => $self->exportnum,
141                             'countrycode' => '1', # vitelity is US/CA only now
142                             'npa'         => $opt{'areacode'},
143                           },
144           });
145
146     my @sorted_rc = sort @rc;
147     return [ @sorted_rc ];
148
149   } elsif ( $opt{'state'} ) { #and not other things, then return areacode
150
151     my @avail = qsearch({
152       'select'   => 'DISTINCT npa',
153       'table'    => 'phone_avail',
154       'hashref'  => { 'exportnum'   => $self->exportnum,
155                       'countrycode' => '1', # vitelity is US/CA only now
156                       'state'       => $opt{'state'},
157                     },
158       'order_by' => 'ORDER BY npa',
159     });
160
161     return [ map $_->npa, @avail ] if @avail; #return cached area codes instead
162
163     #otherwise, search for em
164
165     my $command = 'listavailratecenters';
166     $command = 'listratecenters' if $self->option('fax');
167     my @ratecenters = $self->vitelity_command( $command,
168                                                  'state' => $opt{'state'}, 
169                                              );
170     # XXX: Options: type=unlimited OR type=pri
171
172     if ( $ratecenters[0] eq 'unavailable' || $ratecenters[0] eq 'none' ) {
173       return [];
174     } elsif ( $ratecenters[0] eq 'missingdata' ) {
175       die "missingdata error running Vitelity API"; #die?
176     }
177
178     local $SIG{HUP} = 'IGNORE';
179     local $SIG{INT} = 'IGNORE';
180     local $SIG{QUIT} = 'IGNORE';
181     local $SIG{TERM} = 'IGNORE';
182     local $SIG{TSTP} = 'IGNORE';
183     local $SIG{PIPE} = 'IGNORE';
184
185     my $oldAutoCommit = $FS::UID::AutoCommit;
186     local $FS::UID::AutoCommit = 0;
187     my $dbh = dbh;
188
189     my $errmsg = 'WARNING: error populating phone availability cache: ';
190
191     my %npa = ();
192     foreach my $ratecenter (@ratecenters) {
193
194      my $command = 'listlocal';
195       $command = 'listdids' if $self->option('fax');
196       my @dids = $self->vitelity_command( $command,
197                                             'state'      => $opt{'state'},
198                                             'ratecenter' => $ratecenter,
199                                         );
200     # XXX: Options: type=unlimited OR type=pri
201
202       if ( $dids[0] eq 'unavailable'  || $dids[0] eq 'noneavailable' ) {
203         next;
204       } elsif ( $dids[0] eq 'missingdata' ) {
205         die "missingdata error running Vitelity API"; #die?
206       }
207
208       foreach my $did ( @dids ) {
209         $did =~ /^(\d{3})(\d{3})(\d{4})/ or die "unparsable did $did\n";
210         my($npa, $nxx, $station) = ($1, $2, $3);
211         $npa{$npa}++;
212
213         my $phone_avail = new FS::phone_avail {
214           'exportnum'   => $self->exportnum,
215           'countrycode' => '1', # vitelity is US/CA only now
216           'state'       => $opt{'state'},
217           'npa'         => $npa,
218           'nxx'         => $nxx,
219           'station'     => $station,
220           'name'        => $ratecenter,
221         };
222
223         my $error = $phone_avail->insert();
224         if ( $error ) {
225           $dbh->rollback if $oldAutoCommit;
226           die $errmsg.$error;
227         }
228
229       }
230
231     }
232
233     $dbh->commit or warn $errmsg.$dbh->errstr if $oldAutoCommit;
234
235     my @return = sort { $a <=> $b } keys %npa;
236     return \@return;
237
238   } else {
239     die "get_dids called without state or areacode options";
240   }
241
242 }
243
244 sub vitelity_command {
245   my( $self, $command, @args ) = @_;
246
247   eval "use Net::Vitelity;";
248   die $@ if $@;
249
250   my $vitelity = Net::Vitelity->new(
251     'login' => $self->option('login'),
252     'pass'  => $self->option('pass'),
253     'apitype' => $self->option('fax') ? 'fax' : 'api',
254     #'debug'    => $debug,
255   );
256
257   $vitelity->$command(@args);
258 }
259
260 sub _export_insert {
261   my( $self, $svc_phone ) = (shift, shift);
262
263   return '' if $self->option('dry_run');
264
265   #we want to provision and catch errors now, not queue
266
267   ###
268   # 1. provision the DID
269   ###
270
271   my %vparams = ( 'did' => $svc_phone->phonenum );
272   $vparams{'routesip'} = $self->option('routesip') 
273     if defined $self->option('routesip');
274   $vparams{'type'} = $self->option('type') 
275     if defined $self->option('type');
276
277   my $command = 'getlocaldid';
278   my $success = 'success';
279
280   # this is OK as Vitelity for now is US/CA only; it's not a hack
281   $command = 'gettollfree' if $vparams{'did'} =~ /^800|^888|^877|^866|^855/;
282
283   if ($self->option('fax')) {
284     $command = 'getdid';
285     $success = 'ok';
286   }
287   
288   my $result = $self->vitelity_command($command,%vparams);
289
290   if ( $result ne $success ) {
291     return "Error running Vitelity $command: $result";
292   }
293
294   ###
295   # 2. Provision CNAM
296   ###
297
298   my $cnam_result = $self->vitelity_command('cnamenable',
299                                               'did'=>$svc_phone->phonenum,
300                                            );
301   if ( $result ne 'ok' ) {
302     #we already provisioned the DID, so...
303     warn "Vitelity error enabling CNAM for ". $svc_phone->phonenum. ": $result";
304   }
305
306   ###
307   # 3. Provision E911
308   ###
309
310   my $e911_error = $self->e911_send($svc_phone);
311
312   if ( $e911_error =~ /status=(missingdata|invalid)/i ) {
313
314     my $status = $1;
315     if ( $e911_error =~ /error=(.*)/ ) {
316       $e911_error = "status=$status, error=$1";
317     }
318
319     #but we already provisioned the DID, so:
320     $self->vitelity_command('removedid', 'did'=> $svc_phone->phonenum,);
321     #and check the results?  if it failed, then what?
322
323     return $e911_error;
324   }
325
326   '';
327 }
328
329 sub e911_send {
330   my($self, $svc_phone) = (shift, shift);
331
332   return '' if $self->option('disable_e911');
333
334   my %location = $svc_phone->location_hash;
335   my %e911send = (
336     'did'     => $svc_phone->phonenum,
337     'address' => $location{'address1'},
338     'city'    => $location{'city'},
339     'state'   => $location{'state'},
340     'zip'     => $location{'zip'},
341   );
342   if ( $svc_phone->phone_name ) {
343     $e911send{'name'} = $svc_phone->phone_name;
344   } else {
345     my $cust_main = $svc_phone->cust_svc->cust_pkg->cust_main;
346     $e911send{'name'} = $cust_main->company || $cust_main->first. ' '.
347                                                $cust_main->get('last');
348   }
349   if ( $location{address2} =~ /^\s*(\w+)\W*(\d+)\s*$/ ) {
350     $e911send{'unittype'} = $1;
351     $e911send{'unitnumber'} = $2;
352   }
353
354   my $e911_result = $self->vitelity_command('e911send', %e911send);
355
356   return '' unless $result =~ /^(missingdata|invalid)/i;
357
358   return "Vitelity error provisioning E911 for". $svc_phone->phonenum.
359            ": $result";
360 }
361
362 sub _export_replace {
363   my( $self, $new, $old ) = (shift, shift, shift);
364
365   # Call Forwarding
366   if( $old->forwarddst ne $new->forwarddst ) {
367       my $result = $self->vitelity_command('callfw',
368         'did'           => $old->phonenum,
369         'forward'        => $new->forwarddst ? $new->forwarddst : 'none',
370       );
371       if ( $result ne 'ok' ) {
372         return "Error running Vitelity callfw: $result";
373       }
374   }
375
376   # vfax forwarding emails
377   if( $old->email ne $new->email && $self->option('fax') ) {
378       my $result = $self->vitelity_command('changeemail',
379         'did'           => $old->phonenum,
380         'emails'        => $new->email ? $new->email : '',
381       );
382       if ( $result ne 'ok' ) {
383         return "Error running Vitelity changeemail: $result";
384       }
385   }
386
387   $self->e911_send($new);
388 }
389
390 sub _export_delete {
391   my( $self, $svc_phone ) = (shift, shift);
392
393   return '' if $self->option('dry_run');
394
395   #probably okay to queue the deletion...?
396   #but hell, let's do it inline anyway, who wants phone numbers hanging around
397
398   return 'Deleting vfax DIDs is unsupported by Vitelity API' if $self->option('fax');
399
400   my $result = $self->vitelity_command('removedid',
401     'did'           => $svc_phone->phonenum,
402   );
403
404   if ( $result ne 'success' ) {
405     return "Error running Vitelity removedid: $result";
406   }
407
408   return '' if $self->option('disable_e911');
409
410   '';
411 }
412
413 sub _export_suspend {
414   my( $self, $svc_phone ) = (shift, shift);
415   #nop for now
416   '';
417 }
418
419 sub _export_unsuspend {
420   my( $self, $svc_phone ) = (shift, shift);
421   #nop for now
422   '';
423 }
424
425 1;
426