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