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