2 use base qw(FS::Record);
6 use FS::Record qw( qsearch qsearchs dbh fields );
13 FS::rate - Object methods for rate records
19 $record = new FS::rate \%hash;
20 $record = new FS::rate { 'column' => 'value' };
22 $error = $record->insert;
24 $error = $new_record->replace($old_record);
26 $error = $record->delete;
28 $error = $record->check;
32 An FS::rate object represents an rate plan. FS::rate inherits from
33 FS::Record. The following fields are currently supported:
47 Optional agent (see L<FS::agent>) for agent-virtualized rates.
49 =item default_detailnum
51 Optional rate detail to apply when a call doesn't match any region in the
52 rate plan. If this is not set, the call will either be left unrated (though
53 it may still be processed under a different pricing addon package), or be
54 marked as 'skipped', or throw a fatal error, depending on the setting of
55 the 'ignore_unrateable' package option.
57 Deprecated; we now find the default detail by its lack of regionnum.
69 Creates a new rate plan. To add the rate plan to the database, see L<"insert">.
71 Note that this stores the hash reference, not a distinct copy of the hash it
72 points to. You can ask the object for a copy with the I<hash> method.
76 # the new method can be inherited from FS::Record, if a table method is defined
80 =item insert [ , OPTION => VALUE ... ]
82 Adds this record to the database. If there is an error, returns the error,
83 otherwise returns false.
85 Currently available options are: I<rate_detail>
87 If I<rate_detail> is set to an array reference of FS::rate_detail objects, the
88 objects will have their ratenum field set and will be inserted after this
97 local $SIG{HUP} = 'IGNORE';
98 local $SIG{INT} = 'IGNORE';
99 local $SIG{QUIT} = 'IGNORE';
100 local $SIG{TERM} = 'IGNORE';
101 local $SIG{TSTP} = 'IGNORE';
102 local $SIG{PIPE} = 'IGNORE';
104 my $oldAutoCommit = $FS::UID::AutoCommit;
105 local $FS::UID::AutoCommit = 0;
108 my $error = $self->check;
109 return $error if $error;
111 $error = $self->SUPER::insert;
113 $dbh->rollback if $oldAutoCommit;
117 if ( $options{'rate_detail'} ) {
119 my( $num, $last, $min_sec ) = (0, time, 5); #progressbar foo
121 foreach my $rate_detail ( @{$options{'rate_detail'}} ) {
123 $rate_detail->ratenum($self->ratenum);
124 $error = $rate_detail->insert;
126 $dbh->rollback if $oldAutoCommit;
130 if ( $options{'job'} ) {
132 if ( time - $min_sec > $last ) {
133 my $error = $options{'job'}->update_statustext(
134 int( 100 * $num / scalar( @{$options{'rate_detail'}} ) )
137 $dbh->rollback if $oldAutoCommit;
147 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
156 Delete this record from the database.
160 # the delete method can be inherited from FS::Record
162 =item replace OLD_RECORD [ , OPTION => VALUE ... ]
164 Replaces the OLD_RECORD with this one in the database. If there is an error,
165 returns the error, otherwise returns false.
167 Currently available options are: I<rate_detail>
169 If I<rate_detail> is set to an array reference of FS::rate_detail objects, the
170 objects will have their ratenum field set and will be inserted after this
171 record. Any existing rate_detail records associated with this record will be
177 my ($new, $old) = (shift, shift);
180 local $SIG{HUP} = 'IGNORE';
181 local $SIG{INT} = 'IGNORE';
182 local $SIG{QUIT} = 'IGNORE';
183 local $SIG{TERM} = 'IGNORE';
184 local $SIG{TSTP} = 'IGNORE';
185 local $SIG{PIPE} = 'IGNORE';
187 my $oldAutoCommit = $FS::UID::AutoCommit;
188 local $FS::UID::AutoCommit = 0;
191 # my @old_rate_detail = ();
192 # @old_rate_detail = $old->rate_detail if $options{'rate_detail'};
194 my $error = $new->SUPER::replace($old);
196 $dbh->rollback if $oldAutoCommit;
200 # foreach my $old_rate_detail ( @old_rate_detail ) {
202 # my $error = $old_rate_detail->delete;
204 # $dbh->rollback if $oldAutoCommit;
208 # if ( $options{'job'} ) {
210 # if ( time - $min_sec > $last ) {
211 # my $error = $options{'job'}->update_statustext(
212 # int( 50 * $num / scalar( @old_rate_detail ) )
215 # $dbh->rollback if $oldAutoCommit;
223 if ( $options{'rate_detail'} ) {
224 my $sth = $dbh->prepare('DELETE FROM rate_detail WHERE ratenum = ?') or do {
225 $dbh->rollback if $oldAutoCommit;
229 $sth->execute($old->ratenum) or do {
230 $dbh->rollback if $oldAutoCommit;
234 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
236 foreach my $rate_detail ( @{$options{'rate_detail'}} ) {
238 $rate_detail->ratenum($new->ratenum);
239 $error = $rate_detail->insert;
241 $dbh->rollback if $oldAutoCommit;
245 if ( $options{'job'} ) {
247 if ( time - $min_sec > $last ) {
248 my $error = $options{'job'}->update_statustext(
249 int( 100 * $num / scalar( @{$options{'rate_detail'}} ) )
252 $dbh->rollback if $oldAutoCommit;
263 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
270 Checks all fields to make sure this is a valid rate plan. If there is
271 an error, returns the error, otherwise returns false. Called by the insert
280 $self->ut_numbern('ratenum')
281 || $self->ut_text('ratename')
282 #|| $self->ut_foreign_keyn('agentnum', 'agent', 'agentnum')
283 || $self->ut_numbern('default_detailnum')
285 return $error if $error;
290 =item dest_detail REGIONNUM | RATE_REGION_OBJECTD | HASHREF
292 Returns the rate detail (see L<FS::rate_detail>) for this rate to the
293 specificed destination. If no rate can be found, returns the default
294 rate if there is one, and an empty string otherwise.
296 Destination can be specified as an FS::rate_detail object or regionnum
297 (see L<FS::rate_detail>), or as a hashref containing the following keys:
301 =item I<countrycode> - required.
303 =item I<phonenum> - required.
305 =item I<weektime> - optional. Specifies a time in seconds from the start
306 of the week, and will return a timed rate (one with a non-null I<ratetimenum>)
307 if one exists at that time. If not, returns a non-timed rate.
309 =item I<cdrtypenum> - optional. Specifies a value for the cdrtypenum
310 field, and will return a rate matching that, if one exists. If not, returns
311 a rate with null cdrtypenum.
318 my( $regionnum, $weektime, $cdrtypenum );
319 if ( ref($_[0]) eq 'HASH' ) {
321 my $countrycode = $_[0]->{'countrycode'};
322 my $phonenum = $_[0]->{'phonenum'};
323 $weektime = $_[0]->{'weektime'};
324 $cdrtypenum = $_[0]->{'cdrtypenum'} || '';
326 #find a rate prefix, first look at most specific, then fewer digits,
327 # finally trying the country code only
328 my $rate_prefix = '';
329 $rate_prefix = qsearchs({
330 'table' => 'rate_prefix',
331 'addl_from' => ' JOIN rate_region USING (regionnum)',
333 'countrycode' => $countrycode,
336 'extra_sql' => ' AND exact_match = \'Y\''
339 for my $len ( reverse(1..10) ) {
340 $rate_prefix = qsearchs('rate_prefix', {
341 'countrycode' => $countrycode,
342 #'npa' => { op=> 'LIKE', value=> substr($number, 0, $len) }
343 'npa' => substr($phonenum, 0, $len),
346 $rate_prefix ||= qsearchs('rate_prefix', {
347 'countrycode' => $countrycode,
352 if ( !$rate_prefix ) {
353 # then this call doesn't match any known region; just return the
354 # appropriate anywhere rate
355 return $self->default_detail($cdrtypenum) || $self->default_detail('');
358 $regionnum = $rate_prefix->regionnum;
361 $regionnum = ref($_[0]) ? shift->regionnum : shift;
365 'ratenum' => $self->ratenum,
366 'dest_regionnum' => $regionnum,
369 # find all rates matching ratenum, regionnum, cdrtypenum
370 my @details = qsearch( 'rate_detail', {
372 'cdrtypenum' => $cdrtypenum
374 # failing that, return the global default for this plan with the correct
375 # cdrtypenum (skips weektime processing)
376 if ( !@details and $cdrtypenum ) {
377 my $detail = $self->default_detail($cdrtypenum);
378 return $detail if $detail;
380 # failing that, find all rates maching ratenum, regionnum and null cdrtypenum
381 # (these can have weektime stuff)
382 if ( !@details and $cdrtypenum ) {
383 @details = qsearch( 'rate_detail', {
388 # find one of those matching weektime
389 if ( defined($weektime) ) {
391 my $rate_time = $_->rate_time;
392 $rate_time && $rate_time->contains($weektime)
397 elsif ( @exact > 1 ) {
398 die "overlapping rate_detail times (region $regionnum, time $weektime)\n"
402 # if not found or there is no weektime, find one matching null weektime
404 return $_ if $_->ratetimenum eq '';
406 # if still nothing, return the global default rate for this plan
407 return $self->default_detail('');
412 Returns all region-specific details (see L<FS::rate_detail>) for this rate.
416 =item default_detail [ CDRTYPENUM ]
418 Returns the default rate detail for CDRTYPENUM (or for null CDR type, if not
425 my $cdrtypenum = shift || '';
426 # $self->default_detailnum ?
427 # FS::rate_detail->by_key($self->default_detailnum) : ''
428 qsearchs( 'rate_detail', {
429 ratenum => $self->ratenum,
430 cdrtypenum => $cdrtypenum,
431 dest_regionnum => '',
432 orig_regionnum => '',
442 Job-queue processor for web interface adds/edits
450 warn Dumper($param) if $DEBUG;
452 my $old = qsearchs('rate', { 'ratenum' => $param->{'ratenum'} } )
453 if $param->{'ratenum'};
455 my @rate_detail = map {
457 my $regionnum = $_->regionnum;
458 if ( $param->{"sec_granularity$regionnum"} ) {
460 new FS::rate_detail {
461 'dest_regionnum' => $regionnum,
462 map { $_ => $param->{"$_$regionnum"} }
463 qw( min_included min_charge sec_granularity )
464 #qw( min_included conn_charge conn_sec min_charge sec_granularity )
469 new FS::rate_detail {
470 'dest_regionnum' => $regionnum,
476 'sec_granularity' => '60'
481 } qsearch('rate_region', {} );
483 my $rate = new FS::rate {
484 map { $_ => $param->{$_} }
489 if ( $param->{'ratenum'} ) {
490 warn "$rate replacing $old (". $param->{'ratenum'}. ")\n" if $DEBUG;
492 my @param = ( 'job'=>$job );
494 $rate->default_detailnum($old->default_detailnum);
496 $error = $rate->replace( $old, @param );
499 warn "inserting $rate\n" if $DEBUG;
500 $error = $rate->insert( 'rate_detail' => \@rate_detail,
503 #$ratenum = $rate->getfield('ratenum');
506 die "$error\n" if $error;
514 L<FS::Record>, schema.html from the base documentation.