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