4 use vars qw( @ISA $DEBUG );
5 use FS::Record qw( qsearch qsearchs dbh fields );
14 FS::rate - Object methods for rate records
20 $record = new FS::rate \%hash;
21 $record = new FS::rate { 'column' => 'value' };
23 $error = $record->insert;
25 $error = $new_record->replace($old_record);
27 $error = $record->delete;
29 $error = $record->check;
33 An FS::rate object represents an rate plan. FS::rate inherits from
34 FS::Record. The following fields are currently supported:
48 Optional agent (see L<FS::agent>) for agent-virtualized rates.
50 =item default_detailnum
52 Optional rate detail to apply when a call doesn't match any region in the
53 rate plan. If this is not set, the call will either be left unrated (though
54 it may still be processed under a different pricing addon package), or be
55 marked as 'skipped', or throw a fatal error, depending on the setting of
56 the 'ignore_unrateable' package option.
58 Deprecated; we now find the default detail by its lack of regionnum.
70 Creates a new rate plan. To add the rate plan to the database, see L<"insert">.
72 Note that this stores the hash reference, not a distinct copy of the hash it
73 points to. You can ask the object for a copy with the I<hash> method.
77 # the new method can be inherited from FS::Record, if a table method is defined
81 =item insert [ , OPTION => VALUE ... ]
83 Adds this record to the database. If there is an error, returns the error,
84 otherwise returns false.
86 Currently available options are: I<rate_detail>
88 If I<rate_detail> is set to an array reference of FS::rate_detail objects, the
89 objects will have their ratenum field set and will be inserted after this
98 local $SIG{HUP} = 'IGNORE';
99 local $SIG{INT} = 'IGNORE';
100 local $SIG{QUIT} = 'IGNORE';
101 local $SIG{TERM} = 'IGNORE';
102 local $SIG{TSTP} = 'IGNORE';
103 local $SIG{PIPE} = 'IGNORE';
105 my $oldAutoCommit = $FS::UID::AutoCommit;
106 local $FS::UID::AutoCommit = 0;
109 my $error = $self->check;
110 return $error if $error;
112 $error = $self->SUPER::insert;
114 $dbh->rollback if $oldAutoCommit;
118 if ( $options{'rate_detail'} ) {
120 my( $num, $last, $min_sec ) = (0, time, 5); #progressbar foo
122 foreach my $rate_detail ( @{$options{'rate_detail'}} ) {
124 $rate_detail->ratenum($self->ratenum);
125 $error = $rate_detail->insert;
127 $dbh->rollback if $oldAutoCommit;
131 if ( $options{'job'} ) {
133 if ( time - $min_sec > $last ) {
134 my $error = $options{'job'}->update_statustext(
135 int( 100 * $num / scalar( @{$options{'rate_detail'}} ) )
138 $dbh->rollback if $oldAutoCommit;
148 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
157 Delete this record from the database.
161 # the delete method can be inherited from FS::Record
163 =item replace OLD_RECORD [ , OPTION => VALUE ... ]
165 Replaces the OLD_RECORD with this one in the database. If there is an error,
166 returns the error, otherwise returns false.
168 Currently available options are: I<rate_detail>
170 If I<rate_detail> is set to an array reference of FS::rate_detail objects, the
171 objects will have their ratenum field set and will be inserted after this
172 record. Any existing rate_detail records associated with this record will be
178 my ($new, $old) = (shift, shift);
181 local $SIG{HUP} = 'IGNORE';
182 local $SIG{INT} = 'IGNORE';
183 local $SIG{QUIT} = 'IGNORE';
184 local $SIG{TERM} = 'IGNORE';
185 local $SIG{TSTP} = 'IGNORE';
186 local $SIG{PIPE} = 'IGNORE';
188 my $oldAutoCommit = $FS::UID::AutoCommit;
189 local $FS::UID::AutoCommit = 0;
192 # my @old_rate_detail = ();
193 # @old_rate_detail = $old->rate_detail if $options{'rate_detail'};
195 my $error = $new->SUPER::replace($old);
197 $dbh->rollback if $oldAutoCommit;
201 # foreach my $old_rate_detail ( @old_rate_detail ) {
203 # my $error = $old_rate_detail->delete;
205 # $dbh->rollback if $oldAutoCommit;
209 # if ( $options{'job'} ) {
211 # if ( time - $min_sec > $last ) {
212 # my $error = $options{'job'}->update_statustext(
213 # int( 50 * $num / scalar( @old_rate_detail ) )
216 # $dbh->rollback if $oldAutoCommit;
224 if ( $options{'rate_detail'} ) {
225 my $sth = $dbh->prepare('DELETE FROM rate_detail WHERE ratenum = ?') or do {
226 $dbh->rollback if $oldAutoCommit;
230 $sth->execute($old->ratenum) or do {
231 $dbh->rollback if $oldAutoCommit;
235 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
237 foreach my $rate_detail ( @{$options{'rate_detail'}} ) {
239 $rate_detail->ratenum($new->ratenum);
240 $error = $rate_detail->insert;
242 $dbh->rollback if $oldAutoCommit;
246 if ( $options{'job'} ) {
248 if ( time - $min_sec > $last ) {
249 my $error = $options{'job'}->update_statustext(
250 int( 100 * $num / scalar( @{$options{'rate_detail'}} ) )
253 $dbh->rollback if $oldAutoCommit;
264 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
271 Checks all fields to make sure this is a valid rate plan. If there is
272 an error, returns the error, otherwise returns false. Called by the insert
281 $self->ut_numbern('ratenum')
282 || $self->ut_text('ratename')
283 #|| $self->ut_foreign_keyn('agentnum', 'agent', 'agentnum')
284 || $self->ut_numbern('default_detailnum')
286 return $error if $error;
291 =item dest_detail REGIONNUM | RATE_REGION_OBJECTD | HASHREF
293 Returns the rate detail (see L<FS::rate_detail>) for this rate to the
294 specificed destination. If no rate can be found, returns the default
295 rate if there is one, and an empty string otherwise.
297 Destination can be specified as an FS::rate_detail object or regionnum
298 (see L<FS::rate_detail>), or as a hashref containing the following keys:
302 =item I<countrycode> - required.
304 =item I<phonenum> - required.
306 =item I<weektime> - optional. Specifies a time in seconds from the start
307 of the week, and will return a timed rate (one with a non-null I<ratetimenum>)
308 if one exists at that time. If not, returns a non-timed rate.
310 =item I<cdrtypenum> - optional. Specifies a value for the cdrtypenum
311 field, and will return a rate matching that, if one exists. If not, returns
312 a rate with null cdrtypenum.
319 my( $regionnum, $weektime, $cdrtypenum );
320 if ( ref($_[0]) eq 'HASH' ) {
322 my $countrycode = $_[0]->{'countrycode'};
323 my $phonenum = $_[0]->{'phonenum'};
324 $weektime = $_[0]->{'weektime'};
325 $cdrtypenum = $_[0]->{'cdrtypenum'} || '';
327 #find a rate prefix, first look at most specific, then fewer digits,
328 # finally trying the country code only
329 my $rate_prefix = '';
330 $rate_prefix = qsearchs({
331 'table' => 'rate_prefix',
332 'addl_from' => ' JOIN rate_region USING (regionnum)',
334 'countrycode' => $countrycode,
337 'extra_sql' => ' AND exact_match = \'Y\''
340 for my $len ( reverse(1..10) ) {
341 $rate_prefix = qsearchs('rate_prefix', {
342 'countrycode' => $countrycode,
343 #'npa' => { op=> 'LIKE', value=> substr($number, 0, $len) }
344 'npa' => substr($phonenum, 0, $len),
347 $rate_prefix ||= qsearchs('rate_prefix', {
348 'countrycode' => $countrycode,
353 if ( !$rate_prefix ) {
354 # then this call doesn't match any known region; just return the
355 # appropriate anywhere rate
356 return $self->default_detail($cdrtypenum) || $self->default_detail('');
359 $regionnum = $rate_prefix->regionnum;
362 $regionnum = ref($_[0]) ? shift->regionnum : shift;
366 'ratenum' => $self->ratenum,
367 'dest_regionnum' => $regionnum,
370 # find all rates matching ratenum, regionnum, cdrtypenum
371 my @details = qsearch( 'rate_detail', {
373 'cdrtypenum' => $cdrtypenum
375 # failing that, return the global default for this plan with the correct
376 # cdrtypenum (skips weektime processing)
377 if ( !@details and $cdrtypenum ) {
378 my $detail = $self->default_detail($cdrtypenum);
379 return $detail if $detail;
381 # failing that, find all rates maching ratenum, regionnum and null cdrtypenum
382 # (these can have weektime stuff)
383 if ( !@details and $cdrtypenum ) {
384 @details = qsearch( 'rate_detail', {
389 # find one of those matching weektime
390 if ( defined($weektime) ) {
392 my $rate_time = $_->rate_time;
393 $rate_time && $rate_time->contains($weektime)
398 elsif ( @exact > 1 ) {
399 die "overlapping rate_detail times (region $regionnum, time $weektime)\n"
403 # if not found or there is no weektime, find one matching null weektime
405 return $_ if $_->ratetimenum eq '';
407 # if still nothing, return the global default rate for this plan
408 return $self->default_detail('');
413 Returns all region-specific details (see L<FS::rate_detail>) for this rate.
419 qsearch( 'rate_detail', { 'ratenum' => $self->ratenum } );
428 eval "use FS::agent";
430 qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
435 =item default_detail [ CDRTYPENUM ]
437 Returns the default rate detail for CDRTYPENUM (or for null CDR type, if not
444 my $cdrtypenum = shift || '';
445 # $self->default_detailnum ?
446 # FS::rate_detail->by_key($self->default_detailnum) : ''
447 qsearchs( 'rate_detail', {
448 ratenum => $self->ratenum,
449 cdrtypenum => $cdrtypenum,
450 dest_regionnum => '',
451 orig_regionnum => '',
461 Job-queue processor for web interface adds/edits
465 use Storable qw(thaw);
471 my $param = thaw(decode_base64(shift));
472 warn Dumper($param) if $DEBUG;
474 my $old = qsearchs('rate', { 'ratenum' => $param->{'ratenum'} } )
475 if $param->{'ratenum'};
477 my @rate_detail = map {
479 my $regionnum = $_->regionnum;
480 if ( $param->{"sec_granularity$regionnum"} ) {
482 new FS::rate_detail {
483 'dest_regionnum' => $regionnum,
484 map { $_ => $param->{"$_$regionnum"} }
485 qw( min_included min_charge sec_granularity )
486 #qw( min_included conn_charge conn_sec min_charge sec_granularity )
491 new FS::rate_detail {
492 'dest_regionnum' => $regionnum,
498 'sec_granularity' => '60'
503 } qsearch('rate_region', {} );
505 my $rate = new FS::rate {
506 map { $_ => $param->{$_} }
511 if ( $param->{'ratenum'} ) {
512 warn "$rate replacing $old (". $param->{'ratenum'}. ")\n" if $DEBUG;
514 my @param = ( 'job'=>$job );
516 $rate->default_detailnum($old->default_detailnum);
518 $error = $rate->replace( $old, @param );
521 warn "inserting $rate\n" if $DEBUG;
522 $error = $rate->insert( 'rate_detail' => \@rate_detail,
525 #$ratenum = $rate->getfield('ratenum');
528 die "$error\n" if $error;
536 L<FS::Record>, schema.html from the base documentation.