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