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