bug fixes vitelity.pm, RT4868
[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 ;
18
19 %info = (
20   'svc'     => 'svc_phone',
21   'desc'    => 'Provision phone numbers to Vitelity',
22   'options' => \%options,
23   'notes'   => <<'END'
24 Requires installation of
25 <a href="http://search.cpan.org/dist/Net-Vitelity">Net::Vitelity</a>
26 from CPAN.
27 <br><br>
28 routesip - optional Vitelity sub-account to which newly ordered DIDs will be routed
29 <br>type - optional DID type (perminute, unlimited, or your-pri)
30 END
31 );
32
33 sub rebless { shift; }
34
35 sub get_dids {
36   my $self = shift;
37   my %opt = ref($_[0]) ? %{$_[0]} : @_;
38
39 # currently one of three cases: areacode+exchange, areacode, state
40 # name == ratecenter
41
42   my %search = ();
43
44   my $method = '';
45
46   if ( $opt{'areacode'} && $opt{'exchange'} ) { #return numbers in format NPA-NXX-XXXX
47
48     return [
49       map { join('-', $_->npa, $_->nxx, $_->station ) }
50           qsearch({
51             'table'    => 'phone_avail',
52             'hashref'  => { 'exportnum'   => $self->exportnum,
53                             'countrycode' => '1', # vitelity is US/CA only now
54                             'npa'         => $opt{'areacode'},
55                             'nxx'         => $opt{'exchange'},
56                           },
57             'order_by' => 'ORDER BY station',
58           })
59     ];
60
61   } elsif ( $opt{'areacode'} ) { #return exchanges in format NPA-NXX- literal 'XXXX'
62
63     # you can't call $->name .... that returns "(unlinked)"
64     # and in any case this is still major abuse of encapsulation, it just happens to work for the other fields
65     return [
66            map { $_->{'Hash'}->{name}.' ('. $_->npa. '-'. $_->nxx. '-XXXX)' } 
67           qsearch({
68         # i know this doesn't do the same thing as before, but now the sort works
69             'select'   => 'DISTINCT npa,nxx,name',
70             'table'    => 'phone_avail',
71             'hashref'  => { 'exportnum'   => $self->exportnum,
72                             'countrycode' => '1', # vitelity is US/CA only now
73                             'npa'         => $opt{'areacode'},
74                           },
75             'order_by' => 'ORDER BY nxx',
76           })
77     ];
78
79   } elsif ( $opt{'state'} ) { #and not other things, then return areacode
80
81     #XXX need to flush the cache at some point :/
82
83     my @avail = qsearch({
84       'select'   => 'DISTINCT npa',
85       'table'    => 'phone_avail',
86       'hashref'  => { 'exportnum'   => $self->exportnum,
87                       'countrycode' => '1', # vitelity is US/CA only now
88                       'state'       => $opt{'state'},
89                     },
90       'order_by' => 'ORDER BY npa',
91     });
92
93     return [ map $_->npa, @avail ] if @avail; #return cached area codes instead
94
95     #otherwise, search for em
96
97     my @ratecenters = $self->vitelity_command( 'listavailratecenters',
98                                                  'state' => $opt{'state'}, 
99                                              );
100     # XXX: Options: type=unlimited OR type=pri
101
102     if ( $ratecenters[0] eq 'unavailable' ) {
103       return [];
104     } elsif ( $ratecenters[0] eq 'missingdata' ) {
105       die "missingdata error running Vitelity API"; #die?
106     }
107
108     local $SIG{HUP} = 'IGNORE';
109     local $SIG{INT} = 'IGNORE';
110     local $SIG{QUIT} = 'IGNORE';
111     local $SIG{TERM} = 'IGNORE';
112     local $SIG{TSTP} = 'IGNORE';
113     local $SIG{PIPE} = 'IGNORE';
114
115     my $oldAutoCommit = $FS::UID::AutoCommit;
116     local $FS::UID::AutoCommit = 0;
117     my $dbh = dbh;
118
119     my $errmsg = 'WARNING: error populating phone availability cache: ';
120
121     my %npa = ();
122     foreach my $ratecenter (@ratecenters) {
123
124       my @dids = $self->vitelity_command( 'listlocal',
125                                             'state'      => $opt{'state'},
126                                             'ratecenter' => $ratecenter,
127                                         );
128     # XXX: Options: type=unlimited OR type=pri
129
130       if ( $dids[0] eq 'unavailable' ) {
131         next;
132       } elsif ( $dids[0] eq 'missingdata' ) {
133         die "missingdata error running Vitelity API"; #die?
134       }
135
136       foreach my $did ( @dids ) {
137         $did =~ /^(\d{3})(\d{3})(\d{4}),/ or die "unparsable did $did\n";
138         my($npa, $nxx, $station) = ($1, $2, $3);
139         $npa{$npa}++;
140
141         my $phone_avail = new FS::phone_avail {
142           'exportnum'   => $self->exportnum,
143           'countrycode' => '1', # vitelity is US/CA only now
144           'state'       => $opt{'state'},
145           'npa'         => $npa,
146           'nxx'         => $nxx,
147           'station'     => $station,
148           'name'        => $ratecenter,
149         };
150
151         $error = $phone_avail->insert();
152         if ( $error ) {
153           $dbh->rollback if $oldAutoCommit;
154           die $errmsg.$error;
155         }
156
157       }
158
159     }
160
161     $dbh->commit or warn $errmsg.$dbh->errstr if $oldAutoCommit;
162
163     my @return = sort { $a <=> $b } keys %npa;
164     #@return = sort { (split(' ', $a))[0] <=> (split(' ', $b))[0] } @return;
165
166     return \@return;
167
168   } else {
169     die "get_dids called without state or areacode options";
170   }
171
172 }
173
174 sub vitelity_command {
175   my( $self, $command, @args ) = @_;
176
177   eval "use Net::Vitelity;";
178   die $@ if $@;
179
180   my $vitelity = Net::Vitelity->new(
181     'login' => $self->option('login'),
182     'pass'  => $self->option('pass'),
183     #'debug'    => $debug,
184   );
185
186   $vitelity->$command(@args);
187 }
188
189 sub _export_insert {
190   my( $self, $svc_phone ) = (shift, shift);
191
192   return '' if $self->option('dry_run');
193
194   #we want to provision and catch errors now, not queue
195
196   %vparams = ( 'did' => $svc_phone->phonenum );
197   $vparams{'routesip'} = $self->option('routesip') 
198     if defined $self->option('routesip');
199   $vparams{'type'} = $self->option('type') 
200     if defined $self->option('type');
201
202   my $result = $self->vitelity_command('getlocaldid',%vparams);
203
204   if ( $result ne 'success' ) {
205     return "Error running Vitelity getlocaldid: $result";
206   }
207
208   '';
209 }
210
211 sub _export_replace {
212   my( $self, $new, $old ) = (shift, shift, shift);
213
214   #hmm, what's to change?
215   '';
216 }
217
218 sub _export_delete {
219   my( $self, $svc_phone ) = (shift, shift);
220
221   return '' if $self->option('dry_run');
222
223   #probably okay to queue the deletion...?
224   #but hell, let's do it inline anyway, who wants phone numbers hanging around
225
226   my $result = $self->vitelity_command('removedid',
227     'did'           => $svc_phone->phonenum,
228   );
229
230   if ( $result ne 'success' ) {
231     return "Error running Vitelity getlocaldid: $result";
232   }
233
234   '';
235 }
236
237 sub _export_suspend {
238   my( $self, $svc_phone ) = (shift, shift);
239   #nop for now
240   '';
241 }
242
243 sub _export_unsuspend {
244   my( $self, $svc_phone ) = (shift, shift);
245   #nop for now
246   '';
247 }
248
249 1;
250