stray closing /TABLE in the no-ticket case
[freeside.git] / FS / FS / ClientAPI / PrepaidPhone.pm
1 package FS::ClientAPI::PrepaidPhone;
2
3 use strict;
4 use vars qw($DEBUG $me);
5 use FS::Record qw(qsearchs);
6 use FS::Conf;
7 use FS::rate;
8 use FS::svc_phone;
9
10 $DEBUG = 0;
11 $me = '[FS::ClientAPI::PrepaidPhone]';
12
13 #TODO:
14 # - shared-secret auth? (set a conf value)
15
16 =item call_time HASHREF
17
18 HASHREF contains the following parameters:
19
20 =over 4
21
22 =item src
23
24 Source number (with countrycode)
25
26 =item dst
27
28 Destination number (with countrycode)
29
30 =back
31
32 Always returns a hashref.  If there is an error, the hashref contains a single
33 "error" key with the error message as a value.  Otherwise, returns a hashref
34 with the following keys:
35
36 =over 4
37
38 =item custnum
39
40 Empty if no customer is found associated with the number, customer number
41 otherwise.
42
43 =item seconds
44
45 Number of seconds remaining for a call to destination number
46
47 =back
48
49 =cut
50
51 sub call_time {
52   my $packet = shift;
53
54   my $src = $packet->{'src'};
55   my $dst = $packet->{'dst'};
56
57   my $chargeto;
58   my $rateby;
59   #my $conf = new FS::Conf;
60   #if ( #XXX toll-free?  collect?
61   #  $phonenum = $dst;
62   #} else { #use the src to find the customer
63     $chargeto = $src;
64     $rateby = $dst;
65   #}
66
67   my( $countrycode, $phonenum );
68   if ( $chargeto #an interesting regex to parse out 1&2 digit countrycodes
69          =~ /^(2[078]|3[0-469]|4[013-9]|5[1-8]|6[0-6]|7|8[1-469]|9[0-58])(\d*)$/
70        || $chargeto =~ /^(\d{3})(\d*)$/
71      )
72   {
73     $countrycode = $1;
74     $phonenum = $2;
75   } else { 
76     return { 'error' => "unparsable billing number: $chargeto" };
77   }
78
79
80   my $svc_phone = qsearchs('svc_phone', { 'countrycode' => $countrycode,
81                                           'phonenum'    => $phonenum,
82                                         }
83                           );
84
85   unless ( $svc_phone ) {
86     return { 'error' => "can't find customer for +$countrycode $phonenum" };
87 #    return { 'custnum' => '',
88 #             'seconds' => 0,
89 #             #'balance' => 0,
90 #           };
91   };
92
93   my $cust_pkg = $svc_phone->cust_svc->cust_pkg;
94   my $cust_main = $cust_pkg->cust_main;
95
96   my $part_pkg = $cust_pkg->part_pkg;
97   my @part_pkg = ( $part_pkg, map $_->dst_pkg, $part_pkg->bill_part_pkg_link );
98   #XXX uuh, behavior indeterminate if you have more than one voip_cdr+prefix
99   #add-on, i guess.
100   warn "$me ". scalar(@part_pkg). ': '.
101        join('/', map { $_->plan. $_->option('rating_method') } @part_pkg )
102     if $DEBUG;
103   @part_pkg =
104     grep { $_->plan eq 'voip_cdr' && $_->option('rating_method') eq 'prefix' }
105          @part_pkg;
106
107   my %return = (
108     'custnum' => $cust_pkg->custnum,
109     #'balance' => $cust_pkg->cust_main->balance,
110   );
111
112   warn "$me: ". scalar(@part_pkg). ': '.
113        join('/', map { $_->plan. $_->option('rating_method') } @part_pkg )
114     if $DEBUG;
115   return \%return unless @part_pkg;
116
117   warn "$me searching for rate ". $part_pkg[0]->option('ratenum')
118     if $DEBUG;
119
120   my $rate = qsearchs('rate', { 'ratenum'=>$part_pkg[0]->option('ratenum') } );
121
122   unless ( $rate ) {
123     my $error = 'ratenum '. $part_pkg[0]->option('ratenum'). ' not found';
124     warn "$me $error"
125       if $DEBUG;
126     return { 'error'=>$error };
127   }
128
129   warn "$me found rate ". $rate->ratenum
130     if $DEBUG;
131
132   #rate the call and arrive at a max # of seconds for the customer's balance
133
134   my( $rate_countrycode, $rate_phonenum );
135   if ( $rateby #this is an interesting regex to parse out 1&2 digit countrycodes
136          =~ /^(2[078]|3[0-469]|4[013-9]|5[1-8]|6[0-6]|7|8[1-469]|9[0-58])(\d*)$/
137        || $rateby =~ /^(\d{3})(\d*)$/
138      )
139   {
140     $rate_countrycode = $1;
141     $rate_phonenum = $2;
142   } else { 
143     return { 'error' => "unparsable rating number: $rateby" };
144   }
145
146   my $rate_detail = $rate->dest_detail({ 'countrycode' => $rate_countrycode,
147                                          'phonenum'    => $rate_phonenum,
148                                        });
149   unless ( $rate_detail ) {
150     return { 'error'=>"can't find rate for +$rate_countrycode $rate_phonenum"};
151   }
152
153   unless ( $rate_detail->min_charge > 0 ) {
154     #XXX no charge??  return lots of seconds, a default, 0 or what?
155     #return { 'error' => '0 rate for +$rate_countrycode $rate_phonenum; prepaid service not available" };
156     #customer wants no default for now# $return{'seconds'} = 1800; #half hour?!
157     return \%return;
158   }
159
160   my $balance = FS::ClientAPI::PrepaidPhone->prepaid_phone_balance( $cust_pkg );
161
162   #XXX granularity?  included minutes?  another day...
163   if ( $balance >= 0 ) {
164     return { 'error'=>'No balance' };
165   } else {
166     $return{'seconds'} = int(60 * abs($balance) / $rate_detail->min_charge);
167   }
168
169   warn "$me returning seconds: ". $return{'seconds'};
170
171   return \%return;
172  
173 }
174
175 =item call_time_nanpa 
176
177 Like I<call_time>, except countrycode 1 is not required, and all other
178 countrycodes must be prefixed with 011.
179
180 =cut
181
182 # - everything is assumed to be countrycode 1 unless it starts with 011(ccode)
183 sub call_time_nanpa {
184   my $packet = shift;
185
186   foreach (qw( src dst )) {
187     if ( $packet->{$_} =~ /^011(\d+)/ ) {
188       $packet->{$_} = $1;
189     } elsif ( $packet->{$_} !~ /^1/ ) {
190       $packet->{$_} = '1'.$packet->{$_};
191     }
192   }
193
194   call_time($packet);
195
196 }
197
198 =item phonenum_balance HASHREF
199
200 HASHREF contains the following parameters:
201
202 =over 4
203
204 =item countrycode
205
206 Optional countrycode.  Defaults to 1.
207
208 =item phonenum
209
210 Phone number.
211
212 =back
213
214 Always returns a hashref.  If there is an error, the hashref contains a single
215 "error" key with the error message as a value.  Otherwise, returns a hashref
216 with the following keys:
217
218 =over 4
219
220 =item custnum
221
222 Empty if no customer is found associated with the number, customer number
223 otherwise.
224
225 =item balance
226
227 Customer balance.
228
229 =back
230
231 =cut
232
233 sub phonenum_balance {
234   my $packet = shift;
235
236   warn "$me phonenum_balance called with countrycode ".$packet->{'countrycode'}.
237        " and phonenum ". $packet->{'phonenum'}. "\n"
238     if $DEBUG;
239
240   my $svc_phone = qsearchs('svc_phone', {
241     'countrycode' => ( $packet->{'countrycode'} || 1 ),
242     'phonenum'    => $packet->{'phonenum'},
243   });
244
245   unless ( $svc_phone ) {
246     warn "$me no phone number found\n" if $DEBUG;
247     return { 'custnum' => '',
248              'balance' => 0,
249            };
250   };
251
252   my $cust_pkg = $svc_phone->cust_svc->cust_pkg;
253
254   my $balance = FS::ClientAPI::PrepaidPhone->prepaid_phone_balance( $cust_pkg );
255
256   warn "$me returning $balance balance for pkgnum ".  $cust_pkg->pkgnum.
257                                         ", custnum ". $cust_pkg->custnum
258     if $DEBUG;
259
260   return {
261     'custnum' => $cust_pkg->custnum,
262     'balance' => $balance,
263   };
264
265 }
266
267 sub prepaid_phone_balance {
268   my $class = shift; # i guess
269   my ($cust_pkg) = @_;
270
271   my $conf = new FS::Conf;
272
273   my $pkg_balances = $conf->config_bool('pkg-balances');
274   
275   my $balance = $pkg_balances ? $cust_pkg->balance
276                               : $cust_pkg->cust_main->balance;
277
278   if ( $conf->config_bool('cdr-prerate') ) {
279     my @cust_pkg = $pkg_balances ? ( $cust_pkg )
280                                  : ( $cust_pkg->cust_main->ncancelled_pkgs );
281     foreach my $cust_pkg ( @cust_pkg ) {
282
283       #we only support prerated CDRs with "VOIP/telco CDR rating (standard)"
284       # and "Phone numbers (svc_phone.phonenum)" CDR service matching for now
285       my $part_pkg = $cust_pkg->part_pkg;
286       next unless $part_pkg->plan eq 'voip_cdr'
287                && ($part_pkg->option('cdr_svc_method') || 'svc_phone.phonenum')
288                     eq 'svc_phone.phonenum'
289                && ! $part_pkg->option('bill_inactive_svcs');
290       #XXX skip when there's included minutes
291
292       #select prerated cdrs & subtract them from balance
293
294       # false laziness w/ part_pkg/voip_cdr.pm sorta
295
296       my %options = (
297           'disable_src'    => $part_pkg->option('disable_src'),
298           'default_prefix' => $part_pkg->option('default_prefix'),
299           'cdrtypenum'     => $part_pkg->option('use_cdrtypenum'),
300           'calltypenum'    => $part_pkg->option('use_calltypenum'),
301           'status'         => 'rated',
302           'by_svcnum'      => 1,
303       );  # $last_bill, $$sdate )
304
305       my @cust_svc = grep { $_->part_svc->svcdb eq 'svc_phone' }
306                        $cust_pkg->cust_svc;
307       foreach my $cust_svc ( @cust_svc ) {
308         
309         my $svc_x = $cust_svc->svc_x;
310         my $sum_cdr = $svc_x->sum_cdrs(%options);
311         $balance += $sum_cdr->rated_price;
312
313       }
314
315     }
316   }
317
318   $balance;
319
320 }
321
322 1;