DID selection for fibernetics, RT#19883
[freeside.git] / FS / FS / part_export / vitelity.pm
1 package FS::part_export::vitelity;
2
3 use vars qw(@ISA %info);
4 use Tie::IxHash;
5 use FS::Record qw(qsearch dbh);
6 use FS::part_export;
7 use FS::phone_avail;
8
9 @ISA = qw(FS::part_export);
10
11 tie my %options, 'Tie::IxHash',
12   'login'         => { label=>'Vitelity API login' },
13   'pass'          => { label=>'Vitelity API password' },
14   'dry_run'       => { label=>"Test mode - don't actually provision" },
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                             
23 ;
24
25 %info = (
26   'svc'     => 'svc_phone',
27   'desc'    => 'Provision phone numbers to Vitelity',
28   'options' => \%options,
29   'notes'   => <<'END'
30 Requires installation of
31 <a href="http://search.cpan.org/dist/Net-Vitelity">Net::Vitelity</a>
32 from CPAN.
33 <br><br>
34 routesip - optional Vitelity sub-account to which newly ordered DIDs will be routed
35 <br>type - optional DID type (perminute, unlimited, or your-pri)
36 END
37 );
38
39 sub rebless { shift; }
40
41 sub get_dids_can_tollfree { 1; };
42
43 sub get_dids {
44   my $self = shift;
45   my %opt = ref($_[0]) ? %{$_[0]} : @_;
46
47   if ( $opt{'tollfree'} ) {
48     my $command = 'listtollfree';
49     $command = 'listdids' if $self->option('fax');
50     my @tollfree = $self->vitelity_command($command);
51     my @ret = ();
52
53     return [] if ( $tollfree[0] eq 'noneavailable' || $tollfree[0] eq 'none');
54
55     foreach my $did ( @tollfree ) {
56         $did =~ /^(\d{3})(\d{3})(\d{4})/ or die "unparsable did $did\n";
57         push @ret, $did;
58     }
59
60     my @sorted_ret = sort @ret;
61     return \@sorted_ret;
62
63   } elsif ( $opt{'ratecenter'} && $opt{'state'} ) { 
64
65     my %flushopts = ( 'state' => $opt{'state'}, 
66                     'ratecenter' => $opt{'ratecenter'},
67                     'exportnum' => $self->exportnum
68                   );
69     FS::phone_avail::flush( \%flushopts );
70       
71     local $SIG{HUP} = 'IGNORE';
72     local $SIG{INT} = 'IGNORE';
73     local $SIG{QUIT} = 'IGNORE';
74     local $SIG{TERM} = 'IGNORE';
75     local $SIG{TSTP} = 'IGNORE';
76     local $SIG{PIPE} = 'IGNORE';
77
78     my $oldAutoCommit = $FS::UID::AutoCommit;
79     local $FS::UID::AutoCommit = 0;
80     my $dbh = dbh;
81
82     my $errmsg = 'WARNING: error populating phone availability cache: ';
83
84     my $command = 'listlocal';
85     $command = 'listdids' if $self->option('fax');
86     my @dids = $self->vitelity_command( $command,
87                                         'state'      => $opt{'state'},
88                                         'ratecenter' => $opt{'ratecenter'},
89                                       );
90     # XXX: Options: type=unlimited OR type=pri
91
92     next if ( $dids[0] eq 'unavailable'  || $dids[0] eq 'noneavailable' );
93     die "missingdata error running Vitelity API" if $dids[0] eq 'missingdata';
94
95     foreach my $did ( @dids ) {
96       $did =~ /^(\d{3})(\d{3})(\d{4})/ or die "unparsable did $did\n";
97       my($npa, $nxx, $station) = ($1, $2, $3);
98
99       my $phone_avail = new FS::phone_avail {
100           'exportnum'   => $self->exportnum,
101           'countrycode' => '1', # vitelity is US/CA only now
102           'state'       => $opt{'state'},
103           'npa'         => $npa,
104           'nxx'         => $nxx,
105           'station'     => $station,
106           'name'        => $opt{'ratecenter'},
107       };
108
109       my $error = $phone_avail->insert();
110       if ( $error ) {
111           $dbh->rollback if $oldAutoCommit;
112           die $errmsg.$error;
113       }
114
115     }
116     $dbh->commit or warn $errmsg.$dbh->errstr if $oldAutoCommit;
117
118     return [
119       map { join('-', $_->npa, $_->nxx, $_->station ) }
120           qsearch({
121             'table'    => 'phone_avail',
122             'hashref'  => { 'exportnum'   => $self->exportnum,
123                             'countrycode' => '1', # vitelity is US/CA only now
124                             'name'         => $opt{'ratecenter'},
125                             'state'       => $opt{'state'},
126                           },
127             'order_by' => 'ORDER BY npa, nxx, station',
128           })
129     ];
130
131   } elsif ( $opt{'areacode'} ) { 
132
133     my @rc = map { $_->{'Hash'}->{name}.", ".$_->state } 
134           qsearch({
135             'select'   => 'DISTINCT name, state',
136             'table'    => 'phone_avail',
137             'hashref'  => { 'exportnum'   => $self->exportnum,
138                             'countrycode' => '1', # vitelity is US/CA only now
139                             'npa'         => $opt{'areacode'},
140                           },
141           });
142
143     my @sorted_rc = sort @rc;
144     return [ @sorted_rc ];
145
146   } elsif ( $opt{'state'} ) { #and not other things, then return areacode
147
148     my @avail = qsearch({
149       'select'   => 'DISTINCT npa',
150       'table'    => 'phone_avail',
151       'hashref'  => { 'exportnum'   => $self->exportnum,
152                       'countrycode' => '1', # vitelity is US/CA only now
153                       'state'       => $opt{'state'},
154                     },
155       'order_by' => 'ORDER BY npa',
156     });
157
158     return [ map $_->npa, @avail ] if @avail; #return cached area codes instead
159
160     #otherwise, search for em
161
162     my $command = 'listavailratecenters';
163     $command = 'listratecenters' if $self->option('fax');
164     my @ratecenters = $self->vitelity_command( $command,
165                                                  'state' => $opt{'state'}, 
166                                              );
167     # XXX: Options: type=unlimited OR type=pri
168
169     if ( $ratecenters[0] eq 'unavailable' || $ratecenters[0] eq 'none' ) {
170       return [];
171     } elsif ( $ratecenters[0] eq 'missingdata' ) {
172       die "missingdata error running Vitelity API"; #die?
173     }
174
175     local $SIG{HUP} = 'IGNORE';
176     local $SIG{INT} = 'IGNORE';
177     local $SIG{QUIT} = 'IGNORE';
178     local $SIG{TERM} = 'IGNORE';
179     local $SIG{TSTP} = 'IGNORE';
180     local $SIG{PIPE} = 'IGNORE';
181
182     my $oldAutoCommit = $FS::UID::AutoCommit;
183     local $FS::UID::AutoCommit = 0;
184     my $dbh = dbh;
185
186     my $errmsg = 'WARNING: error populating phone availability cache: ';
187
188     my %npa = ();
189     foreach my $ratecenter (@ratecenters) {
190
191      my $command = 'listlocal';
192       $command = 'listdids' if $self->option('fax');
193       my @dids = $self->vitelity_command( $command,
194                                             'state'      => $opt{'state'},
195                                             'ratecenter' => $ratecenter,
196                                         );
197     # XXX: Options: type=unlimited OR type=pri
198
199       if ( $dids[0] eq 'unavailable'  || $dids[0] eq 'noneavailable' ) {
200         next;
201       } elsif ( $dids[0] eq 'missingdata' ) {
202         die "missingdata error running Vitelity API"; #die?
203       }
204
205       foreach my $did ( @dids ) {
206         $did =~ /^(\d{3})(\d{3})(\d{4})/ or die "unparsable did $did\n";
207         my($npa, $nxx, $station) = ($1, $2, $3);
208         $npa{$npa}++;
209
210         my $phone_avail = new FS::phone_avail {
211           'exportnum'   => $self->exportnum,
212           'countrycode' => '1', # vitelity is US/CA only now
213           'state'       => $opt{'state'},
214           'npa'         => $npa,
215           'nxx'         => $nxx,
216           'station'     => $station,
217           'name'        => $ratecenter,
218         };
219
220         my $error = $phone_avail->insert();
221         if ( $error ) {
222           $dbh->rollback if $oldAutoCommit;
223           die $errmsg.$error;
224         }
225
226       }
227
228     }
229
230     $dbh->commit or warn $errmsg.$dbh->errstr if $oldAutoCommit;
231
232     my @return = sort { $a <=> $b } keys %npa;
233     return \@return;
234
235   } else {
236     die "get_dids called without state or areacode options";
237   }
238
239 }
240
241 sub vitelity_command {
242   my( $self, $command, @args ) = @_;
243
244   eval "use Net::Vitelity;";
245   die $@ if $@;
246
247   my $vitelity = Net::Vitelity->new(
248     'login' => $self->option('login'),
249     'pass'  => $self->option('pass'),
250     'apitype' => $self->option('fax') ? 'fax' : 'api',
251     #'debug'    => $debug,
252   );
253
254   $vitelity->$command(@args);
255 }
256
257 sub _export_insert {
258   my( $self, $svc_phone ) = (shift, shift);
259
260   return '' if $self->option('dry_run');
261
262   #we want to provision and catch errors now, not queue
263
264   my %vparams = ( 'did' => $svc_phone->phonenum );
265   $vparams{'routesip'} = $self->option('routesip') 
266     if defined $self->option('routesip');
267   $vparams{'type'} = $self->option('type') 
268     if defined $self->option('type');
269
270   my $command = 'getlocaldid';
271   my $success = 'success';
272
273   # this is OK as Vitelity for now is US/CA only; it's not a hack
274   $command = 'gettollfree' if $vparams{'did'} =~ /^800|^888|^877|^866|^855/;
275
276   if($self->option('fax')) {
277         $command = 'getdid';
278         $success = 'ok';
279   }
280   
281   my $result = $self->vitelity_command($command,%vparams);
282
283   if ( $result ne $success ) {
284     return "Error running Vitelity $command: $result";
285   }
286
287   '';
288 }
289
290 sub _export_replace {
291   my( $self, $new, $old ) = (shift, shift, shift);
292
293   # Call Forwarding
294   if( $old->forwarddst ne $new->forwarddst ) {
295       my $result = $self->vitelity_command('callfw',
296         'did'           => $old->phonenum,
297         'forward'       => $new->forwarddst ? $new->forwarddst : 'none',
298       );
299       if ( $result ne 'ok' ) {
300         return "Error running Vitelity callfw: $result";
301       }
302   }
303
304   # vfax forwarding emails
305   if( $old->email ne $new->email && $self->option('fax') ) {
306       my $result = $self->vitelity_command('changeemail',
307         'did'           => $old->phonenum,
308         'emails'        => $new->email ? $new->email : '',
309       );
310       if ( $result ne 'ok' ) {
311         return "Error running Vitelity changeemail: $result";
312       }
313   }
314
315   '';
316 }
317
318 sub _export_delete {
319   my( $self, $svc_phone ) = (shift, shift);
320
321   return '' if $self->option('dry_run');
322
323   #probably okay to queue the deletion...?
324   #but hell, let's do it inline anyway, who wants phone numbers hanging around
325
326   return 'Deleting vfax DIDs is unsupported by Vitelity API' if $self->option('fax');
327
328   my $result = $self->vitelity_command('removedid',
329     'did'           => $svc_phone->phonenum,
330   );
331
332   if ( $result ne 'success' ) {
333     return "Error running Vitelity removedid: $result";
334   }
335
336   '';
337 }
338
339 sub _export_suspend {
340   my( $self, $svc_phone ) = (shift, shift);
341   #nop for now
342   '';
343 }
344
345 sub _export_unsuspend {
346   my( $self, $svc_phone ) = (shift, shift);
347   #nop for now
348   '';
349 }
350
351 1;
352