1 package FS::part_export::vitelity;
2 use base qw( FS::part_export );
6 use Geo::StreetAddress::US;
7 use FS::Record qw( qsearch dbh );
11 tie my %options, 'Tie::IxHash',
12 'login' => { label=>'Vitelity API login' },
13 'pass' => { label=>'Vitelity API password' },
14 'routesip' => { label=>'routesip (optional sub-account)' },
15 'type' => { label=>'type (optional DID type to order)' },
16 'fax' => { label=>'vfax service', type=>'checkbox' },
17 'restrict_selection' => { type => 'select',
18 label => 'Restrict DID Selection',
19 options => [ '', 'tollfree', 'non-tollfree' ],
21 'dry_run' => { label => "Test mode - don't actually provision",
24 'disable_e911' => { label => "Disable E911 provisioning",
31 'desc' => 'Provision phone numbers to Vitelity',
32 'options' => \%options,
35 Requires installation of
36 <a href="http://search.cpan.org/dist/Net-Vitelity">Net::Vitelity</a>
39 routesip - optional Vitelity sub-account to which newly ordered DIDs will be routed
40 <br>type - optional DID type (perminute, unlimited, or your-pri)
44 sub rebless { shift; }
46 sub can_get_dids { 1; }
47 sub get_dids_can_tollfree { 1; };
51 my %opt = ref($_[0]) ? %{$_[0]} : @_;
53 if ( $opt{'tollfree'} ) {
54 my $command = 'listtollfree';
55 $command = 'listdids' if $self->option('fax');
56 my @tollfree = $self->vitelity_command($command);
59 return [] if ( $tollfree[0] eq 'noneavailable' || $tollfree[0] eq 'none');
61 foreach my $did ( @tollfree ) {
62 $did =~ /^(\d{3})(\d{3})(\d{4})/ or die "unparsable did $did\n";
66 my @sorted_ret = sort @ret;
69 } elsif ( $opt{'ratecenter'} && $opt{'state'} ) {
71 my %flushopts = ( 'state' => $opt{'state'},
72 'ratecenter' => $opt{'ratecenter'},
73 'exportnum' => $self->exportnum
75 FS::phone_avail::flush( \%flushopts );
77 local $SIG{HUP} = 'IGNORE';
78 local $SIG{INT} = 'IGNORE';
79 local $SIG{QUIT} = 'IGNORE';
80 local $SIG{TERM} = 'IGNORE';
81 local $SIG{TSTP} = 'IGNORE';
82 local $SIG{PIPE} = 'IGNORE';
84 my $oldAutoCommit = $FS::UID::AutoCommit;
85 local $FS::UID::AutoCommit = 0;
88 my $errmsg = 'WARNING: error populating phone availability cache: ';
90 my $command = 'listlocal';
91 $command = 'listdids' if $self->option('fax');
92 my @dids = $self->vitelity_command( $command,
93 'state' => $opt{'state'},
94 'ratecenter' => $opt{'ratecenter'},
96 # XXX: Options: type=unlimited OR type=pri
98 next if ( $dids[0] eq 'unavailable' || $dids[0] eq 'noneavailable' );
99 die "missingdata error running Vitelity API" if $dids[0] eq 'missingdata';
101 foreach my $did ( @dids ) {
102 $did =~ /^(\d{3})(\d{3})(\d{4})/ or die "unparsable did $did\n";
103 my($npa, $nxx, $station) = ($1, $2, $3);
105 my $phone_avail = new FS::phone_avail {
106 'exportnum' => $self->exportnum,
107 'countrycode' => '1', # vitelity is US/CA only now
108 'state' => $opt{'state'},
111 'station' => $station,
112 'name' => $opt{'ratecenter'},
115 my $error = $phone_avail->insert();
117 $dbh->rollback if $oldAutoCommit;
122 $dbh->commit or warn $errmsg.$dbh->errstr if $oldAutoCommit;
125 map { join('-', $_->npa, $_->nxx, $_->station ) }
127 'table' => 'phone_avail',
128 'hashref' => { 'exportnum' => $self->exportnum,
129 'countrycode' => '1', # vitelity is US/CA only now
130 'name' => $opt{'ratecenter'},
131 'state' => $opt{'state'},
133 'order_by' => 'ORDER BY npa, nxx, station',
137 } elsif ( $opt{'areacode'} ) {
139 my @rc = map { $_->{'Hash'}->{name}.", ".$_->state }
141 'select' => 'DISTINCT name, state',
142 'table' => 'phone_avail',
143 'hashref' => { 'exportnum' => $self->exportnum,
144 'countrycode' => '1', # vitelity is US/CA only now
145 'npa' => $opt{'areacode'},
149 my @sorted_rc = sort @rc;
150 return [ @sorted_rc ];
152 } elsif ( $opt{'state'} ) { #and not other things, then return areacode
154 my @avail = qsearch({
155 'select' => 'DISTINCT npa',
156 'table' => 'phone_avail',
157 'hashref' => { 'exportnum' => $self->exportnum,
158 'countrycode' => '1', # vitelity is US/CA only now
159 'state' => $opt{'state'},
161 'order_by' => 'ORDER BY npa',
164 return [ map $_->npa, @avail ] if @avail; #return cached area codes instead
166 #otherwise, search for em
168 my $command = 'listavailratecenters';
169 $command = 'listratecenters' if $self->option('fax');
170 my @ratecenters = $self->vitelity_command( $command,
171 'state' => $opt{'state'},
173 # XXX: Options: type=unlimited OR type=pri
175 if ( $ratecenters[0] eq 'unavailable' || $ratecenters[0] eq 'none' ) {
177 } elsif ( $ratecenters[0] eq 'missingdata' ) {
178 die "missingdata error running Vitelity API"; #die?
181 local $SIG{HUP} = 'IGNORE';
182 local $SIG{INT} = 'IGNORE';
183 local $SIG{QUIT} = 'IGNORE';
184 local $SIG{TERM} = 'IGNORE';
185 local $SIG{TSTP} = 'IGNORE';
186 local $SIG{PIPE} = 'IGNORE';
188 my $oldAutoCommit = $FS::UID::AutoCommit;
189 local $FS::UID::AutoCommit = 0;
192 my $errmsg = 'WARNING: error populating phone availability cache: ';
195 foreach my $ratecenter (@ratecenters) {
197 my $command = 'listlocal';
198 $command = 'listdids' if $self->option('fax');
199 my @dids = $self->vitelity_command( $command,
200 'state' => $opt{'state'},
201 'ratecenter' => $ratecenter,
203 # XXX: Options: type=unlimited OR type=pri
205 if ( $dids[0] eq 'unavailable' || $dids[0] eq 'noneavailable' ) {
207 } elsif ( $dids[0] eq 'missingdata' ) {
208 die "missingdata error running Vitelity API"; #die?
211 foreach my $did ( @dids ) {
212 $did =~ /^(\d{3})(\d{3})(\d{4})/ or die "unparsable did $did\n";
213 my($npa, $nxx, $station) = ($1, $2, $3);
216 my $phone_avail = new FS::phone_avail {
217 'exportnum' => $self->exportnum,
218 'countrycode' => '1', # vitelity is US/CA only now
219 'state' => $opt{'state'},
222 'station' => $station,
223 'name' => $ratecenter,
226 my $error = $phone_avail->insert();
228 $dbh->rollback if $oldAutoCommit;
236 $dbh->commit or warn $errmsg.$dbh->errstr if $oldAutoCommit;
238 my @return = sort { $a <=> $b } keys %npa;
242 die "get_dids called without state or areacode options";
247 sub vitelity_command {
248 my( $self, $command, @args ) = @_;
250 eval "use Net::Vitelity;";
253 my $vitelity = Net::Vitelity->new(
254 'login' => $self->option('login'),
255 'pass' => $self->option('pass'),
256 'apitype' => $self->option('fax') ? 'fax' : 'api',
260 $vitelity->$command(@args);
263 sub vitelity_lnp_command {
264 my( $self, $command, @args ) = @_;
266 eval "use Net::Vitelity 0.04;";
269 my $vitelity = Net::Vitelity->new(
270 'login' => $self->option('login'),
271 'pass' => $self->option('pass'),
276 $vitelity->$command(@args);
280 my( $self, $svc_phone ) = (shift, shift);
282 return '' if $self->option('dry_run');
284 #we want to provision and catch errors now, not queue
286 #porting a number in? different code path
287 if ( $svc_phone->lnp_status eq 'portingin' ) {
289 my %location = $svc_phone->location_hash;
290 my $sa = Geo::StreetAddress::US->parse_location( $location{'address1'} );
292 my $result = $self->vitelity_lnp_command('addport',
293 'portnumber' => $svc_phone->phonenum,
296 'carrier' => $svc_phone->lnp_other_provider,
297 'company' => $svc_phone->cust_svc->cust_pkg->cust_main->company,
298 'accnumber' => $svc_phone->lnp_other_provider_account,
299 'name' => $svc_phone->phone_name_or_cust,
300 'streetnumber' => $sa->{number},
301 'streetprefix' => $sa->{prefix},
302 'streetname' => $sa->{street}. ' '. $street{type},
303 'streetsuffix' => $sa->{suffix},
304 'unit' => ( $sa->{sec_unit_num}
305 ? $sa->{sec_unit_type}. ' '. $sa->{sec_unit_num}
308 'city' => $location{'city'},
309 'state' => $location{'state'},
310 'zip' => $location{'zip'},
311 'billnumber' => $svc_phone->phonenum, #?? do we need a new field for this?
312 'contactnumber' => $svc_phone->cust_svc->cust_pkg->cust_main->daytime,
315 if ( $result =~ /^ok:/i ) {
316 my($ok, $portid, $sig, $bill) = split(':', $result);
317 $svc_phone->lnp_portid($portid);
318 $svc_phone->lnp_signature('Y') if $sig =~ /y/i;
319 $svc_phone->lnp_bill('Y') if $bill =~ /y/i;
320 return $svc_phone->replace;
322 return "Error initiating Vitelity port: $result";
328 # 1. provision the DID
331 my %vparams = ( 'did' => $svc_phone->phonenum );
332 $vparams{'routesip'} = $self->option('routesip')
333 if defined $self->option('routesip');
334 $vparams{'type'} = $self->option('type')
335 if defined $self->option('type');
337 my $command = 'getlocaldid';
338 my $success = 'success';
340 # this is OK as Vitelity for now is US/CA only; it's not a hack
341 $command = 'gettollfree' if $vparams{'did'} =~ /^800|^888|^877|^866|^855/;
343 if ($self->option('fax')) {
348 my $result = $self->vitelity_command($command,%vparams);
350 if ( $result ne $success ) {
351 return "Error running Vitelity $command: $result";
358 my $cnam_result = $self->vitelity_command('cnamenable',
359 'did'=>$svc_phone->phonenum,
361 if ( $result ne 'ok' ) {
362 #we already provisioned the DID, so...
363 warn "Vitelity error enabling CNAM for ". $svc_phone->phonenum. ": $result";
370 my $e911_error = $self->e911_send($svc_phone);
372 if ( $e911_error =~ /^(missingdata|invalid)/i ) {
373 #but we already provisioned the DID, so:
374 $self->vitelity_command('removedid', 'did'=> $svc_phone->phonenum,);
375 #and check the results? if it failed, then what?
384 my($self, $svc_phone) = (shift, shift);
386 return '' if $self->option('disable_e911');
388 my %location = $svc_phone->location_hash;
390 'did' => $svc_phone->phonenum,
391 'name' => $svc_phone->phone_name_or_cust,
392 'address' => $location{'address1'},
393 'city' => $location{'city'},
394 'state' => $location{'state'},
395 'zip' => $location{'zip'},
397 if ( $location{address2} =~ /^\s*(\w+)\W*(\d+)\s*$/ ) {
398 $e911send{'unittype'} = $1;
399 $e911send{'unitnumber'} = $2;
402 my $e911_result = $self->vitelity_command('e911send', %e911send);
404 return '' unless $result =~ /^(missingdata|invalid)/i;
406 return "Vitelity error provisioning E911 for". $svc_phone->phonenum.
410 sub _export_replace {
411 my( $self, $new, $old ) = (shift, shift, shift);
414 if( $old->forwarddst ne $new->forwarddst ) {
415 my $result = $self->vitelity_command('callfw',
416 'did' => $old->phonenum,
417 'forward' => $new->forwarddst ? $new->forwarddst : 'none',
419 if ( $result ne 'ok' ) {
420 return "Error running Vitelity callfw: $result";
424 # vfax forwarding emails
425 if( $old->email ne $new->email && $self->option('fax') ) {
426 my $result = $self->vitelity_command('changeemail',
427 'did' => $old->phonenum,
428 'emails' => $new->email ? $new->email : '',
430 if ( $result ne 'ok' ) {
431 return "Error running Vitelity changeemail: $result";
435 $self->e911_send($new);
439 my( $self, $svc_phone ) = (shift, shift);
441 return '' if $self->option('dry_run');
443 #probably okay to queue the deletion...?
444 #but hell, let's do it inline anyway, who wants phone numbers hanging around
446 return 'Deleting vfax DIDs is unsupported by Vitelity API' if $self->option('fax');
448 my $result = $self->vitelity_command('removedid',
449 'did' => $svc_phone->phonenum,
452 if ( $result ne 'success' ) {
453 return "Error running Vitelity removedid: $result";
456 return '' if $self->option('disable_e911');
461 sub _export_suspend {
462 my( $self, $svc_phone ) = (shift, shift);
467 sub _export_unsuspend {
468 my( $self, $svc_phone ) = (shift, shift);
476 my $in_svcpart = 'IN ('. join( ',', map $_->svcpart, $self->export_svc). ')';
478 foreach my $svc_phone (
479 qsearch({ 'table' => 'svc_phone',
480 'hashref' => {lnp_status=>'portingin'},
481 'extra_sql' => "AND svcpart $in_svcpart",
485 my $result = $self->vitelity_lnp_command('checkstatus',
486 'portid'=>$svc_phone->lnp_portid,
489 #XXX what $result values mean the port is done?
491 if ( $result =~ /^complete$/ ) { #"complete"? nfi
493 $svc_phone->lnp_status('portedin');
494 my $error = $self->_export_insert($svc_phone);
496 #XXX log this using our internal log instead, so we can alert on it
498 warn "ERROR provisioning ported-in DID ". $svc_phone->phonenum. ": $error";
500 $error = $svc_phone->replace; #to set the lnp_status
501 #XXX log this using our internal log instead, so we can alert on it
502 warn "ERROR setting lnp_status for DID ". $svc_phone->phonenum. ": $error" if $error;