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