additional vitelity debugging, RT#73618
[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 Dumper($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       return $svc_phone->replace;
334     } else {
335       return "Error initiating Vitelity port: $result";
336     }
337
338   }
339
340   ###
341   # 1. provision the DID
342   ###
343
344   my %vparams = ( 'did' => $svc_phone->phonenum );
345   $vparams{'routesip'} = $self->option('routesip') 
346     if defined $self->option('routesip');
347   $vparams{'type'} = $self->option('type') 
348     if defined $self->option('type');
349
350   my $command = 'getlocaldid';
351   my $success = 'success';
352
353   # this is OK as Vitelity for now is US/CA only; it's not a hack
354   $command = 'gettollfree' if $vparams{'did'} =~ /^800|^888|^877|^866|^855/;
355
356   if ($self->option('fax')) {
357     $command = 'getdid';
358     $success = 'ok';
359   }
360   
361   my $result = $self->vitelity_command($command,%vparams);
362
363   if ( $result ne $success ) {
364     return "Error running Vitelity $command: $result";
365   }
366
367   ###
368   # 2. Provision CNAM
369   ###
370
371   my $cnam_result = $self->vitelity_command('cnamenable',
372                                               'did'=>$svc_phone->phonenum,
373                                            );
374   if ( $result ne 'ok' ) {
375     #we already provisioned the DID, so...
376     warn "Vitelity error enabling CNAM for ". $svc_phone->phonenum. ": $result";
377   }
378
379   ###
380   # 3. Provision E911
381   ###
382
383   my $e911_error = $self->e911_send($svc_phone);
384
385   if ( $e911_error =~ /^(missingdata|invalid)/i ) {
386     #but we already provisioned the DID, so:
387     $self->vitelity_command('removedid', 'did'=> $svc_phone->phonenum,);
388     #and check the results?  if it failed, then what?
389
390     return $e911_error;
391   }
392
393   '';
394 }
395
396 sub e911send {
397   my($self, $svc_phone) = (shift, shift);
398
399   return '' if $self->option('disable_e911');
400
401   my %location = $svc_phone->location_hash;
402   my %e911send = (
403     'did'     => $svc_phone->phonenum,
404     'name'    => $svc_phone->phone_name_or_cust,
405     'address' => $location{'address1'},
406     'city'    => $location{'city'},
407     'state'   => $location{'state'},
408     'zip'     => $location{'zip'},
409   );
410   if ( $location{address2} =~ /^\s*(\w+)\W*(\d+)\s*$/ ) {
411     $e911send{'unittype'} = $1;
412     $e911send{'unitnumber'} = $2;
413   }
414
415   my $e911_result = $self->vitelity_command('e911send', %e911send);
416
417   return '' unless $result =~ /^(missingdata|invalid)/i;
418
419   return "Vitelity error provisioning E911 for". $svc_phone->phonenum.
420            ": $result";
421 }
422
423 sub _export_replace {
424   my( $self, $new, $old ) = (shift, shift, shift);
425
426   # Call Forwarding
427   if( $old->forwarddst ne $new->forwarddst ) {
428       my $result = $self->vitelity_command('callfw',
429         'did'           => $old->phonenum,
430         'forward'        => $new->forwarddst ? $new->forwarddst : 'none',
431       );
432       if ( $result ne 'ok' ) {
433         return "Error running Vitelity callfw: $result";
434       }
435   }
436
437   # vfax forwarding emails
438   if( $old->email ne $new->email && $self->option('fax') ) {
439       my $result = $self->vitelity_command('changeemail',
440         'did'           => $old->phonenum,
441         'emails'        => $new->email ? $new->email : '',
442       );
443       if ( $result ne 'ok' ) {
444         return "Error running Vitelity changeemail: $result";
445       }
446   }
447
448   $self->e911_send($new);
449 }
450
451 sub _export_delete {
452   my( $self, $svc_phone ) = (shift, shift);
453
454   return '' if $self->option('dry_run');
455
456   #probably okay to queue the deletion...?
457   #but hell, let's do it inline anyway, who wants phone numbers hanging around
458
459   return 'Deleting vfax DIDs is unsupported by Vitelity API' if $self->option('fax');
460
461   my $result = $self->vitelity_command('removedid',
462     'did'           => $svc_phone->phonenum,
463   );
464
465   if ( $result ne 'success' ) {
466     return "Error running Vitelity removedid: $result";
467   }
468
469   return '' if $self->option('disable_e911');
470
471   '';
472 }
473
474 sub _export_suspend {
475   my( $self, $svc_phone ) = (shift, shift);
476   #nop for now
477   '';
478 }
479
480 sub _export_unsuspend {
481   my( $self, $svc_phone ) = (shift, shift);
482   #nop for now
483   '';
484 }
485
486 sub check_lnp {
487   my $self = shift;
488
489   my @export_svc = $self->export_svc;
490   return unless @export_svc;
491
492   my $in_svcpart = 'IN ('. join( ',', map $_->svcpart, @export_svc). ')';
493
494   foreach my $svc_phone (
495     qsearch({ 'table'     => 'svc_phone',
496               'addl_from' => 'LEFT JOIN cust_svc USING (svcnum)',
497               'hashref'   => {lnp_status=>'portingin'},
498               'extra_sql' => "AND svcpart $in_svcpart",
499            })
500   ) {
501
502     my $result = $self->vitelity_lnp_command('checkstatus',
503                                                'portid'=>$svc_phone->lnp_portid,
504                                             );
505
506     if ( $result =~ /^Complete/i ) {
507
508       $svc_phone->lnp_status('portedin');
509       my $error = $self->_export_insert($svc_phone);
510       if ( $error ) {
511         #XXX log this using our internal log instead, so we can alert on it
512         # properly
513         warn "ERROR provisioning ported-in DID ". $svc_phone->phonenum. ": $error";
514       } else {
515         $error = $svc_phone->replace; #to set the lnp_status
516         #XXX log this using our internal log instead, so we can alert on it
517         warn "ERROR setting lnp_status for DID ". $svc_phone->phonenum. ": $error" if $error;
518       }
519
520     } elsif ( $result ne $svc_phone->lnp_reject_reason ) {
521       $svc_phone->lnp_reject_reason($result);
522       $error = $svc_phone->replace;
523       #XXX log this using our internal log instead, so we can alert on it
524       warn "ERROR setting lnp_reject_reason for DID ". $svc_phone->phonenum. ": $error" if $error;
525
526     }
527
528   }
529
530 }
531
532 1;
533