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.
67 Creates a new rate plan. To add the rate plan to the database, see L<"insert">.
69 Note that this stores the hash reference, not a distinct copy of the hash it
70 points to. You can ask the object for a copy with the I<hash> method.
74 # the new method can be inherited from FS::Record, if a table method is defined
78 =item insert [ , OPTION => VALUE ... ]
80 Adds this record to the database. If there is an error, returns the error,
81 otherwise returns false.
83 Currently available options are: I<rate_detail>
85 If I<rate_detail> is set to an array reference of FS::rate_detail objects, the
86 objects will have their ratenum field set and will be inserted after this
95 local $SIG{HUP} = 'IGNORE';
96 local $SIG{INT} = 'IGNORE';
97 local $SIG{QUIT} = 'IGNORE';
98 local $SIG{TERM} = 'IGNORE';
99 local $SIG{TSTP} = 'IGNORE';
100 local $SIG{PIPE} = 'IGNORE';
102 my $oldAutoCommit = $FS::UID::AutoCommit;
103 local $FS::UID::AutoCommit = 0;
106 my $error = $self->check;
107 return $error if $error;
109 $error = $self->SUPER::insert;
111 $dbh->rollback if $oldAutoCommit;
115 if ( $options{'rate_detail'} ) {
117 my( $num, $last, $min_sec ) = (0, time, 5); #progressbar foo
119 foreach my $rate_detail ( @{$options{'rate_detail'}} ) {
121 $rate_detail->ratenum($self->ratenum);
122 $error = $rate_detail->insert;
124 $dbh->rollback if $oldAutoCommit;
128 if ( $options{'job'} ) {
130 if ( time - $min_sec > $last ) {
131 my $error = $options{'job'}->update_statustext(
132 int( 100 * $num / scalar( @{$options{'rate_detail'}} ) )
135 $dbh->rollback if $oldAutoCommit;
145 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
154 Delete this record from the database.
158 # the delete method can be inherited from FS::Record
160 =item replace OLD_RECORD [ , OPTION => VALUE ... ]
162 Replaces the OLD_RECORD with this one in the database. If there is an error,
163 returns the error, otherwise returns false.
165 Currently available options are: I<rate_detail>
167 If I<rate_detail> is set to an array reference of FS::rate_detail objects, the
168 objects will have their ratenum field set and will be inserted after this
169 record. Any existing rate_detail records associated with this record will be
175 my ($new, $old) = (shift, shift);
178 local $SIG{HUP} = 'IGNORE';
179 local $SIG{INT} = 'IGNORE';
180 local $SIG{QUIT} = 'IGNORE';
181 local $SIG{TERM} = 'IGNORE';
182 local $SIG{TSTP} = 'IGNORE';
183 local $SIG{PIPE} = 'IGNORE';
185 my $oldAutoCommit = $FS::UID::AutoCommit;
186 local $FS::UID::AutoCommit = 0;
189 # my @old_rate_detail = ();
190 # @old_rate_detail = $old->rate_detail if $options{'rate_detail'};
192 my $error = $new->SUPER::replace($old);
194 $dbh->rollback if $oldAutoCommit;
198 # foreach my $old_rate_detail ( @old_rate_detail ) {
200 # my $error = $old_rate_detail->delete;
202 # $dbh->rollback if $oldAutoCommit;
206 # if ( $options{'job'} ) {
208 # if ( time - $min_sec > $last ) {
209 # my $error = $options{'job'}->update_statustext(
210 # int( 50 * $num / scalar( @old_rate_detail ) )
213 # $dbh->rollback if $oldAutoCommit;
221 if ( $options{'rate_detail'} ) {
222 my $sth = $dbh->prepare('DELETE FROM rate_detail WHERE ratenum = ?') or do {
223 $dbh->rollback if $oldAutoCommit;
227 $sth->execute($old->ratenum) or do {
228 $dbh->rollback if $oldAutoCommit;
232 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
234 foreach my $rate_detail ( @{$options{'rate_detail'}} ) {
236 $rate_detail->ratenum($new->ratenum);
237 $error = $rate_detail->insert;
239 $dbh->rollback if $oldAutoCommit;
243 if ( $options{'job'} ) {
245 if ( time - $min_sec > $last ) {
246 my $error = $options{'job'}->update_statustext(
247 int( 100 * $num / scalar( @{$options{'rate_detail'}} ) )
250 $dbh->rollback if $oldAutoCommit;
261 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
268 Checks all fields to make sure this is a valid rate plan. If there is
269 an error, returns the error, otherwise returns false. Called by the insert
278 $self->ut_numbern('ratenum')
279 || $self->ut_text('ratename')
280 #|| $self->ut_foreign_keyn('agentnum', 'agent', 'agentnum')
281 || $self->ut_numbern('default_detailnum')
283 return $error if $error;
288 =item dest_detail REGIONNUM | RATE_REGION_OBJECTD | HASHREF
290 Returns the rate detail (see L<FS::rate_detail>) for this rate to the
291 specificed destination. If no rate can be found, returns the default
292 rate if there is one, and an empty string otherwise.
294 Destination can be specified as an FS::rate_detail object or regionnum
295 (see L<FS::rate_detail>), or as a hashref containing the following keys:
299 =item I<countrycode> - required.
301 =item I<phonenum> - required.
303 =item I<weektime> - optional. Specifies a time in seconds from the start
304 of the week, and will return a timed rate (one with a non-null I<ratetimenum>)
305 if one exists at that time. If not, returns a non-timed rate.
307 =item I<cdrtypenum> - optional. Specifies a value for the cdrtypenum
308 field, and will return a rate matching that, if one exists. If not, returns
309 a rate with null cdrtypenum.
316 my( $regionnum, $weektime, $cdrtypenum );
317 if ( ref($_[0]) eq 'HASH' ) {
319 my $countrycode = $_[0]->{'countrycode'};
320 my $phonenum = $_[0]->{'phonenum'};
321 $weektime = $_[0]->{'weektime'};
322 $cdrtypenum = $_[0]->{'cdrtypenum'} || '';
324 #find a rate prefix, first look at most specific, then fewer digits,
325 # finally trying the country code only
326 my $rate_prefix = '';
327 $rate_prefix = qsearchs({
328 'table' => 'rate_prefix',
329 'addl_from' => ' JOIN rate_region USING (regionnum)',
331 'countrycode' => $countrycode,
334 'extra_sql' => ' AND exact_match = \'Y\''
337 for my $len ( reverse(1..10) ) {
338 $rate_prefix = qsearchs('rate_prefix', {
339 'countrycode' => $countrycode,
340 #'npa' => { op=> 'LIKE', value=> substr($number, 0, $len) }
341 'npa' => substr($phonenum, 0, $len),
344 $rate_prefix ||= qsearchs('rate_prefix', {
345 'countrycode' => $countrycode,
350 return '' unless $rate_prefix;
352 $regionnum = $rate_prefix->regionnum;
355 $regionnum = ref($_[0]) ? shift->regionnum : shift;
359 'ratenum' => $self->ratenum,
360 'dest_regionnum' => $regionnum,
363 # find all rates matching ratenum, regionnum, cdrtypenum
364 my @details = qsearch( 'rate_detail', {
366 'cdrtypenum' => $cdrtypenum
368 # find all rates maching ratenum, regionnum and null cdrtypenum
369 if ( !@details and $cdrtypenum ) {
370 @details = qsearch( 'rate_detail', {
375 # find one of those matching weektime
376 if ( defined($weektime) ) {
378 my $rate_time = $_->rate_time;
379 $rate_time && $rate_time->contains($weektime)
384 elsif ( @exact > 1 ) {
385 die "overlapping rate_detail times (region $regionnum, time $weektime)\n"
389 # if not found or there is no weektime, find one matching null weektime
391 return $_ if $_->ratetimenum eq '';
393 # if still nothing, return the global default rate for this plan
394 return $self->default_detail;
399 Returns all region-specific details (see L<FS::rate_detail>) for this rate.
405 Returns the default rate detail, if there is one.
411 $self->default_detailnum ?
412 FS::rate_detail->by_key($self->default_detailnum) : ''
421 Job-queue processor for web interface adds/edits
429 warn Dumper($param) if $DEBUG;
431 my $old = qsearchs('rate', { 'ratenum' => $param->{'ratenum'} } )
432 if $param->{'ratenum'};
434 my @rate_detail = map {
436 my $regionnum = $_->regionnum;
437 if ( $param->{"sec_granularity$regionnum"} ) {
439 new FS::rate_detail {
440 'dest_regionnum' => $regionnum,
441 map { $_ => $param->{"$_$regionnum"} }
442 qw( min_included min_charge sec_granularity )
443 #qw( min_included conn_charge conn_sec min_charge sec_granularity )
448 new FS::rate_detail {
449 'dest_regionnum' => $regionnum,
455 'sec_granularity' => '60'
460 } qsearch('rate_region', {} );
462 my $rate = new FS::rate {
463 map { $_ => $param->{$_} }
468 if ( $param->{'ratenum'} ) {
469 warn "$rate replacing $old (". $param->{'ratenum'}. ")\n" if $DEBUG;
471 my @param = ( 'job'=>$job );
472 push @param, 'rate_detail'=>\@rate_detail
473 unless $param->{'preserve_rate_detail'};
475 $error = $rate->replace( $old, @param );
478 warn "inserting $rate\n" if $DEBUG;
479 $error = $rate->insert( 'rate_detail' => \@rate_detail,
482 #$ratenum = $rate->getfield('ratenum');
485 die "$error\n" if $error;
493 L<FS::Record>, schema.html from the base documentation.