add e911 to vitelity integration, RT#73618, 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 Data::Dumper;
7 use Geo::StreetAddress::US;
8 use Net::Vitelity 0.05;
9 use FS::Record qw( qsearch dbh );
10 use FS::phone_avail;
11 use FS::svc_phone;
12
13 tie my %options, 'Tie::IxHash',
14   'login'              => { label=>'Vitelity API login' },
15   'pass'               => { label=>'Vitelity API password' },
16   'routesip'           => { label=>'routesip (optional sub-account)' },
17   'type'               => { label=>'type (optional DID type to order)' },
18   'fax'                => { label=>'vfax service', type=>'checkbox' },
19   'restrict_selection' => { type    => 'select',
20                             label   => 'Restrict DID Selection', 
21                             options => [ '', 'tollfree', 'non-tollfree' ],
22                           },
23   'dry_run'            => { label => "Test mode - don't actually provision",
24                             type  => 'checkbox',
25                           },
26   'disable_e911'       => { label => "Disable E911 provisioning",
27                             type  => 'checkbox',
28                           },
29   'debug'              => { label  => 'Enable debugging',
30                              type  => 'checkbox',
31                              value => 1,
32                           },
33 ;
34
35 %info = (
36   'svc'        => 'svc_phone',
37   'desc'       => 'Provision phone numbers to Vitelity',
38   'options'    => \%options,
39   'no_machine' => 1,
40   'notes'      => <<'END'
41 routesip - optional Vitelity sub-account to which newly ordered DIDs will be routed
42 <br>type - optional DID type (perminute, unlimited, or your-pri)
43 END
44 );
45
46 sub rebless { shift; }
47
48 sub can_get_dids { 1; }
49 sub get_dids_can_tollfree { 1; };
50 sub can_lnp { 1; }
51
52 sub get_dids {
53   my $self = shift;
54   my %opt = ref($_[0]) ? %{$_[0]} : @_;
55
56   if ( $opt{'tollfree'} ) {
57     my $command = 'listtollfree';
58     $command = 'listdids' if $self->option('fax');
59     my @tollfree = $self->vitelity_command($command);
60     my @ret = ();
61
62     return [] if ( $tollfree[0] eq 'noneavailable' || $tollfree[0] eq 'none');
63
64     foreach my $did ( @tollfree ) {
65         $did =~ /^(\d{3})(\d{3})(\d{4})/ or die "unparsable toll-free did $did\n";
66         push @ret, $did;
67     }
68
69     my @sorted_ret = sort @ret;
70     return \@sorted_ret;
71
72   } elsif ( $opt{'ratecenter'} && $opt{'state'} ) { 
73
74     my %flushopts = ( 'state' => $opt{'state'}, 
75                     'ratecenter' => $opt{'ratecenter'},
76                     'exportnum' => $self->exportnum
77                   );
78     FS::phone_avail::flush( \%flushopts );
79       
80     local $SIG{HUP} = 'IGNORE';
81     local $SIG{INT} = 'IGNORE';
82     local $SIG{QUIT} = 'IGNORE';
83     local $SIG{TERM} = 'IGNORE';
84     local $SIG{TSTP} = 'IGNORE';
85     local $SIG{PIPE} = 'IGNORE';
86
87     my $oldAutoCommit = $FS::UID::AutoCommit;
88     local $FS::UID::AutoCommit = 0;
89     my $dbh = dbh;
90
91     my $errmsg = 'WARNING: error populating phone availability cache: ';
92
93     my $command = 'listlocal';
94     $command = 'listdids' if $self->option('fax');
95     my @dids = $self->vitelity_command( $command,
96                                         'state'      => $opt{'state'},
97                                         'ratecenter' => $opt{'ratecenter'},
98                                       );
99     # XXX: Options: type=unlimited OR type=pri
100
101     next if ( $dids[0] eq 'unavailable'  || $dids[0] eq 'noneavailable' );
102     die "missingdata error running Vitelity API" if $dids[0] eq 'missingdata';
103
104     foreach my $did ( @dids ) {
105       $did =~ /^(\d{3})(\d{3})(\d{4})/ or die "unparsable (state and ratecenter) did $did\n";
106       my($npa, $nxx, $station) = ($1, $2, $3);
107
108       my $phone_avail = new FS::phone_avail {
109           'exportnum'   => $self->exportnum,
110           'countrycode' => '1', # vitelity is US/CA only now
111           'state'       => $opt{'state'},
112           'npa'         => $npa,
113           'nxx'         => $nxx,
114           'station'     => $station,
115           'name'        => $opt{'ratecenter'},
116       };
117
118       my $error = $phone_avail->insert();
119       if ( $error ) {
120           $dbh->rollback if $oldAutoCommit;
121           die $errmsg.$error;
122       }
123
124     }
125     $dbh->commit or warn $errmsg.$dbh->errstr if $oldAutoCommit;
126
127     return [
128       map { join('-', $_->npa, $_->nxx, $_->station ) }
129           qsearch({
130             'table'    => 'phone_avail',
131             'hashref'  => { 'exportnum'   => $self->exportnum,
132                             'countrycode' => '1', # vitelity is US/CA only now
133                             'name'         => $opt{'ratecenter'},
134                             'state'          => $opt{'state'},
135                           },
136             'order_by' => 'ORDER BY npa, nxx, station',
137           })
138     ];
139
140   } elsif ( $opt{'areacode'} ) { 
141
142     my @rc = map { $_->{'Hash'}->{name}.", ".$_->state } 
143           qsearch({
144             'select'   => 'DISTINCT name, state',
145             'table'    => 'phone_avail',
146             'hashref'  => { 'exportnum'   => $self->exportnum,
147                             'countrycode' => '1', # vitelity is US/CA only now
148                             'npa'         => $opt{'areacode'},
149                           },
150           });
151
152     my @sorted_rc = sort @rc;
153     return [ @sorted_rc ];
154
155   } elsif ( $opt{'state'} ) { #and not other things, then return areacode
156
157     my @avail = qsearch({
158       'select'   => 'DISTINCT npa',
159       'table'    => 'phone_avail',
160       'hashref'  => { 'exportnum'   => $self->exportnum,
161                       'countrycode' => '1', # vitelity is US/CA only now
162                       'state'       => $opt{'state'},
163                     },
164       'order_by' => 'ORDER BY npa',
165     });
166
167     return [ map $_->npa, @avail ] if @avail; #return cached area codes instead
168
169     #otherwise, search for em
170
171     my $command = 'listavailratecenters';
172     $command = 'listratecenters' if $self->option('fax');
173     my @ratecenters = $self->vitelity_command( $command,
174                                                  'state' => $opt{'state'}, 
175                                              );
176     # XXX: Options: type=unlimited OR type=pri
177
178     if ( $ratecenters[0] eq 'unavailable' || $ratecenters[0] eq 'none' ) {
179       return [];
180     } elsif ( $ratecenters[0] eq 'missingdata' ) {
181       die "missingdata error running Vitelity API"; #die?
182     }
183
184     local $SIG{HUP} = 'IGNORE';
185     local $SIG{INT} = 'IGNORE';
186     local $SIG{QUIT} = 'IGNORE';
187     local $SIG{TERM} = 'IGNORE';
188     local $SIG{TSTP} = 'IGNORE';
189     local $SIG{PIPE} = 'IGNORE';
190
191     my $oldAutoCommit = $FS::UID::AutoCommit;
192     local $FS::UID::AutoCommit = 0;
193     my $dbh = dbh;
194
195     my $errmsg = 'WARNING: error populating phone availability cache: ';
196
197     my %npa = ();
198     foreach my $ratecenter (@ratecenters) {
199
200      my $command = 'listlocal';
201       $command = 'listdids' if $self->option('fax');
202       my @dids = $self->vitelity_command( $command,
203                                             'state'      => $opt{'state'},
204                                             'ratecenter' => $ratecenter,
205                                         );
206     # XXX: Options: type=unlimited OR type=pri
207
208       if ( $dids[0] eq 'unavailable'  || $dids[0] eq 'noneavailable' ) {
209         next;
210       } elsif ( $dids[0] eq 'missingdata' ) {
211         die "missingdata error running Vitelity API"; #die?
212       }
213
214       foreach my $did ( @dids ) {
215         $did =~ /^(\d{3})(\d{3})(\d{4})/ or die "unparsable (state) did $did\n";
216         my($npa, $nxx, $station) = ($1, $2, $3);
217         $npa{$npa}++;
218
219         my $phone_avail = new FS::phone_avail {
220           'exportnum'   => $self->exportnum,
221           'countrycode' => '1', # vitelity is US/CA only now
222           'state'       => $opt{'state'},
223           'npa'         => $npa,
224           'nxx'         => $nxx,
225           'station'     => $station,
226           'name'        => $ratecenter,
227         };
228
229         my $error = $phone_avail->insert();
230         if ( $error ) {
231           $dbh->rollback if $oldAutoCommit;
232           die $errmsg.$error;
233         }
234
235       }
236
237     }
238
239     $dbh->commit or warn $errmsg.$dbh->errstr if $oldAutoCommit;
240
241     my @return = sort { $a <=> $b } keys %npa;
242     return \@return;
243
244   } else {
245     die "get_dids called without state or areacode options";
246   }
247
248 }
249
250 sub vitelity_command {
251   my( $self, $command, @args ) = @_;
252
253   my $vitelity = Net::Vitelity->new(
254     'login'    => $self->option('login'),
255     'pass'     => $self->option('pass'),
256     'apitype'  => $self->option('fax') ? 'fax' : 'api',
257     'debug'    => $self->option('debug'),
258   );
259
260   $vitelity->$command(@args);
261 }
262
263 sub vitelity_lnp_command {
264   my( $self, $command, @args ) = @_;
265
266   my $vitelity = Net::Vitelity->new(
267     'login'    => $self->option('login'),
268     'pass'     => $self->option('pass'),
269     'apitype'  => 'lnp',
270     'debug'    => $self->option('debug'),
271   );
272
273   $vitelity->$command(@args);
274 }
275
276 sub _export_insert {
277   my( $self, $svc_phone ) = (shift, shift);
278
279   return '' if $self->option('dry_run');
280
281   #we want to provision and catch errors now, not queue
282
283   #porting a number in?  different code path
284   if ( $svc_phone->lnp_status eq 'portingin' ) {
285
286     my $cust_main = $svc_phone->cust_svc->cust_pkg->cust_main;
287
288     return 'Customer company is required'
289       unless $cust_main->company;
290
291     return 'Customer day phone (for contact, not porting) is required'
292       unless $cust_main->daytime;
293
294     return 'LNP Other Provider is required'
295       unless $svc_phone->lnp_other_provider;
296
297     return 'LNP Other Provider Account # is required'
298       unless $svc_phone->lnp_other_provider_account;
299
300     my %location = $svc_phone->location_hash;
301     my $sa = Geo::StreetAddress::US->parse_location( $location{'address1'} );
302
303     my $result = $self->vitelity_lnp_command('addport',
304       'portnumber'    => $svc_phone->phonenum,
305       'partial'       => 'no',
306       'wireless'      => 'no',
307       'carrier'       => $svc_phone->lnp_other_provider,
308       'company'       => $cust_main->company,
309       'accnumber'     => $svc_phone->lnp_other_provider_account,
310       'name'          => $svc_phone->phone_name_or_cust,
311       'streetnumber'  => $sa->{number},
312       'streetprefix'  => $sa->{prefix},
313       'streetname'    => $sa->{street}. ' '. $street{type},
314       'streetsuffix'  => $sa->{suffix},
315       'unit'          => ( $sa->{sec_unit_num}
316                              ? $sa->{sec_unit_type}. ' '. $sa->{sec_unit_num}
317                              : ''
318                          ),
319       'city'          => $location{'city'},
320       'state'         => $location{'state'},
321       'zip'           => $location{'zip'},
322       'billnumber'    => $svc_phone->phonenum, #?? do we need a new field for this?
323       'contactnumber' => $cust_main->daytime,
324     );
325
326     warn "Vitelity reponse: $result" if $self->option('debug');
327
328     if ( $result =~ /^ok:/i ) {
329       my($ok, $portid, $sig, $bill) = split(':', $result);
330       $svc_phone->lnp_portid($portid);
331       $svc_phone->lnp_signature('Y') if $sig  =~ /y/i;
332       $svc_phone->lnp_bill('Y')      if $bill =~ /y/i;
333       local($FS::svc_Common::noexport_hack) = 1;
334       return $svc_phone->replace;
335     } else {
336       return "Error initiating Vitelity port: $result";
337     }
338
339   }
340
341   ###
342   # 1. provision the DID
343   ###
344
345   my %vparams = ( 'did' => $svc_phone->phonenum );
346   $vparams{'routesip'} = $self->option('routesip') 
347     if defined $self->option('routesip');
348   $vparams{'type'} = $self->option('type') 
349     if defined $self->option('type');
350
351   my $command = 'getlocaldid';
352   my $success = 'success';
353
354   # this is OK as Vitelity for now is US/CA only; it's not a hack
355   $command = 'gettollfree' if $vparams{'did'} =~ /^800|^888|^877|^866|^855/;
356
357   if ($self->option('fax')) {
358     $command = 'getdid';
359     $success = 'ok';
360   }
361   
362   my $result = $self->vitelity_command($command,%vparams);
363
364   if ( $result ne $success ) {
365     return "Error running Vitelity $command: $result";
366   }
367
368   ###
369   # 2. Provision CNAM
370   ###
371
372   my $cnam_result = $self->vitelity_command('cnamenable',
373                                               'did'=>$svc_phone->phonenum,
374                                            );
375   if ( $result ne 'ok' ) {
376     #we already provisioned the DID, so...
377     warn "Vitelity error enabling CNAM for ". $svc_phone->phonenum. ": $result";
378   }
379
380   ###
381   # 3. Provision E911
382   ###
383
384   my $e911_error = $self->e911_send($svc_phone);
385
386   if ( $e911_error =~ /^(missingdata|invalid)/i ) {
387     #but we already provisioned the DID, so:
388     $self->vitelity_command('removedid', 'did'=> $svc_phone->phonenum,);
389     #and check the results?  if it failed, then what?
390
391     return $e911_error;
392   }
393
394   '';
395 }
396
397 sub e911_send {
398   my($self, $svc_phone) = (shift, shift);
399
400   return '' if $self->option('disable_e911');
401
402   my %location = $svc_phone->location_hash;
403   my %e911send = (
404     'did'     => $svc_phone->phonenum,
405     'name'    => $svc_phone->phone_name_or_cust,
406     'address' => $location{'address1'},
407     'city'    => $location{'city'},
408     'state'   => $location{'state'},
409     'zip'     => $location{'zip'},
410   );
411   if ( $location{address2} =~ /^\s*(\w+)\W*(\d+)\s*$/ ) {
412     $e911send{'unittype'} = $1;
413     $e911send{'unitnumber'} = $2;
414   }
415
416   my $e911_result = $self->vitelity_command('e911send', %e911send);
417
418   return '' unless $result =~ /^(missingdata|invalid)/i;
419
420   return "Vitelity error provisioning E911 for". $svc_phone->phonenum.
421            ": $result";
422 }
423
424 sub _export_replace {
425   my( $self, $new, $old ) = (shift, shift, shift);
426
427   # Call Forwarding
428   if( $old->forwarddst ne $new->forwarddst ) {
429       my $result = $self->vitelity_command('callfw',
430         'did'           => $old->phonenum,
431         'forward'        => $new->forwarddst ? $new->forwarddst : 'none',
432       );
433       if ( $result ne 'ok' ) {
434         return "Error running Vitelity callfw: $result";
435       }
436   }
437
438   # vfax forwarding emails
439   if( $old->email ne $new->email && $self->option('fax') ) {
440       my $result = $self->vitelity_command('changeemail',
441         'did'           => $old->phonenum,
442         'emails'        => $new->email ? $new->email : '',
443       );
444       if ( $result ne 'ok' ) {
445         return "Error running Vitelity changeemail: $result";
446       }
447   }
448
449   $self->e911_send($new);
450 }
451
452 sub _export_delete {
453   my( $self, $svc_phone ) = (shift, shift);
454
455   return '' if $self->option('dry_run');
456
457   #probably okay to queue the deletion...?
458   #but hell, let's do it inline anyway, who wants phone numbers hanging around
459
460   return 'Deleting vfax DIDs is unsupported by Vitelity API' if $self->option('fax');
461
462   my $result = $self->vitelity_command('removedid',
463     'did'           => $svc_phone->phonenum,
464   );
465
466   if ( $result ne 'success' ) {
467     return "Error running Vitelity removedid: $result";
468   }
469
470   return '' if $self->option('disable_e911');
471
472   '';
473 }
474
475 sub _export_suspend {
476   my( $self, $svc_phone ) = (shift, shift);
477   #nop for now
478   '';
479 }
480
481 sub _export_unsuspend {
482   my( $self, $svc_phone ) = (shift, shift);
483   #nop for now
484   '';
485 }
486
487 sub check_lnp {
488   my $self = shift;
489
490   my @export_svc = $self->export_svc;
491   return unless @export_svc;
492
493   my $in_svcpart = 'IN ('. join( ',', map $_->svcpart, @export_svc). ')';
494
495   foreach my $svc_phone (
496     qsearch({ 'table'     => 'svc_phone',
497               'addl_from' => 'LEFT JOIN cust_svc USING (svcnum)',
498               'hashref'   => {lnp_status=>'portingin'},
499               'extra_sql' => "AND svcpart $in_svcpart",
500            })
501   ) {
502
503     my $result = $self->vitelity_lnp_command('checkstatus',
504                                                'portid'=>$svc_phone->lnp_portid,
505                                             );
506
507     if ( $result =~ /^Complete/i ) {
508
509       $svc_phone->lnp_status('portedin');
510       my $error = $self->_export_insert($svc_phone);
511       if ( $error ) {
512         #XXX log this using our internal log instead, so we can alert on it
513         # properly
514         warn "ERROR provisioning ported-in DID ". $svc_phone->phonenum. ": $error";
515       } else {
516         local($FS::svc_Common::noexport_hack) = 1;
517         $error = $svc_phone->replace; #to set the lnp_status
518         #XXX log this using our internal log instead, so we can alert on it
519         warn "ERROR setting lnp_status for DID ". $svc_phone->phonenum. ": $error" if $error;
520       }
521
522     } elsif ( $result ne $svc_phone->lnp_reject_reason ) {
523       $svc_phone->lnp_reject_reason($result);
524       local($FS::svc_Common::noexport_hack) = 1;
525       $error = $svc_phone->replace;
526       #XXX log this using our internal log instead, so we can alert on it
527       warn "ERROR setting lnp_reject_reason for DID ". $svc_phone->phonenum. ": $error" if $error;
528
529     }
530
531   }
532
533 }
534
535 1;
536