add e911 (and cnameenable) to vitelity integration, 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 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 can_get_dids { 1; }
45 sub get_dids_can_tollfree { 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 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 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 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   eval "use Net::Vitelity;";
249   die $@ if $@;
250
251   my $vitelity = Net::Vitelity->new(
252     'login' => $self->option('login'),
253     'pass'  => $self->option('pass'),
254     'apitype' => $self->option('fax') ? 'fax' : 'api',
255     #'debug'    => $debug,
256   );
257
258   $vitelity->$command(@args);
259 }
260
261 sub _export_insert {
262   my( $self, $svc_phone ) = (shift, shift);
263
264   return '' if $self->option('dry_run');
265
266   #we want to provision and catch errors now, not queue
267
268   ###
269   # 1. provision the DID
270   ###
271
272   my %vparams = ( 'did' => $svc_phone->phonenum );
273   $vparams{'routesip'} = $self->option('routesip') 
274     if defined $self->option('routesip');
275   $vparams{'type'} = $self->option('type') 
276     if defined $self->option('type');
277
278   my $command = 'getlocaldid';
279   my $success = 'success';
280
281   # this is OK as Vitelity for now is US/CA only; it's not a hack
282   $command = 'gettollfree' if $vparams{'did'} =~ /^800|^888|^877|^866|^855/;
283
284   if ($self->option('fax')) {
285     $command = 'getdid';
286     $success = 'ok';
287   }
288   
289   my $result = $self->vitelity_command($command,%vparams);
290
291   if ( $result ne $success ) {
292     return "Error running Vitelity $command: $result";
293   }
294
295   ###
296   # 2. Provision CNAM
297   ###
298
299   my $cnam_result = $self->vitelity_command('cnamenable',
300                                               'did'=>$svc_phone->phonenum,
301                                            );
302   if ( $result ne 'ok' ) {
303     #we already provisioned the DID, so...
304     warn "Vitelity error enabling CNAM for ". $svc_phone->phonenum. ": $result";
305   }
306
307   ###
308   # 3. Provision E911
309   ###
310
311   my $e911_error = $self->e911_send($svc_phone);
312
313   if ( $e911_error =~ /^(missingdata|invalid)/i ) {
314     #but we already provisioned the DID, so:
315     $self->vitelity_command('removedid', 'did'=> $svc_phone->phonenum,);
316     #and check the results?  if it failed, then what?
317
318     return $e911_error;
319   }
320
321   '';
322 }
323
324 sub e911send {
325   my($self, $svc_phone) = (shift, shift);
326
327   return '' if $self->option('disable_e911');
328
329   my %location = $svc_phone->location_hash;
330   my %e911send = (
331     'did'     => $svc_phone->phonenum,
332     'address' => $location{'address1'},
333     'city'    => $location{'city'},
334     'state'   => $location{'state'},
335     'zip'     => $location{'zip'},
336   );
337   if ( $svc_phone->phone_name ) {
338     $e911send{'name'} = $svc_phone->phone_name;
339   } else {
340     my $cust_main = $svc_phone->cust_svc->cust_pkg->cust_main;
341     $e911send{'name'} = $cust_main->company || $cust_main->first. ' '.
342                                                $cust_main->get('last');
343   }
344   if ( $location{address2} =~ /^\s*(\w+)\W*(\d+)\s*$/ ) {
345     $e911send{'unittype'} = $1;
346     $e911send{'unitnumber'} = $2;
347   }
348
349   my $e911_result = $self->vitelity_command('e911send', %e911send);
350
351   return '' unless $result =~ /^(missingdata|invalid)/i;
352
353   return "Vitelity error provisioning E911 for". $svc_phone->phonenum.
354            ": $result";
355 }
356
357 sub _export_replace {
358   my( $self, $new, $old ) = (shift, shift, shift);
359
360   # Call Forwarding
361   if( $old->forwarddst ne $new->forwarddst ) {
362       my $result = $self->vitelity_command('callfw',
363         'did'           => $old->phonenum,
364         'forward'        => $new->forwarddst ? $new->forwarddst : 'none',
365       );
366       if ( $result ne 'ok' ) {
367         return "Error running Vitelity callfw: $result";
368       }
369   }
370
371   # vfax forwarding emails
372   if( $old->email ne $new->email && $self->option('fax') ) {
373       my $result = $self->vitelity_command('changeemail',
374         'did'           => $old->phonenum,
375         'emails'        => $new->email ? $new->email : '',
376       );
377       if ( $result ne 'ok' ) {
378         return "Error running Vitelity changeemail: $result";
379       }
380   }
381
382   $self->e911_send($new);
383 }
384
385 sub _export_delete {
386   my( $self, $svc_phone ) = (shift, shift);
387
388   return '' if $self->option('dry_run');
389
390   #probably okay to queue the deletion...?
391   #but hell, let's do it inline anyway, who wants phone numbers hanging around
392
393   return 'Deleting vfax DIDs is unsupported by Vitelity API' if $self->option('fax');
394
395   my $result = $self->vitelity_command('removedid',
396     'did'           => $svc_phone->phonenum,
397   );
398
399   if ( $result ne 'success' ) {
400     return "Error running Vitelity removedid: $result";
401   }
402
403   return '' if $self->option('disable_e911');
404
405   '';
406 }
407
408 sub _export_suspend {
409   my( $self, $svc_phone ) = (shift, shift);
410   #nop for now
411   '';
412 }
413
414 sub _export_unsuspend {
415   my( $self, $svc_phone ) = (shift, shift);
416   #nop for now
417   '';
418 }
419
420 1;
421