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