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.
68 Creates a new rate plan. To add the rate plan to the database, see L<"insert">.
70 Note that this stores the hash reference, not a distinct copy of the hash it
71 points to. You can ask the object for a copy with the I<hash> method.
75 # the new method can be inherited from FS::Record, if a table method is defined
79 =item insert [ , OPTION => VALUE ... ]
81 Adds this record to the database. If there is an error, returns the error,
82 otherwise returns false.
84 Currently available options are: I<rate_detail>
86 If I<rate_detail> is set to an array reference of FS::rate_detail objects, the
87 objects will have their ratenum field set and will be inserted after this
96 local $SIG{HUP} = 'IGNORE';
97 local $SIG{INT} = 'IGNORE';
98 local $SIG{QUIT} = 'IGNORE';
99 local $SIG{TERM} = 'IGNORE';
100 local $SIG{TSTP} = 'IGNORE';
101 local $SIG{PIPE} = 'IGNORE';
103 my $oldAutoCommit = $FS::UID::AutoCommit;
104 local $FS::UID::AutoCommit = 0;
107 my $error = $self->check;
108 return $error if $error;
110 $error = $self->SUPER::insert;
112 $dbh->rollback if $oldAutoCommit;
116 if ( $options{'rate_detail'} ) {
118 my( $num, $last, $min_sec ) = (0, time, 5); #progressbar foo
120 foreach my $rate_detail ( @{$options{'rate_detail'}} ) {
122 $rate_detail->ratenum($self->ratenum);
123 $error = $rate_detail->insert;
125 $dbh->rollback if $oldAutoCommit;
129 if ( $options{'job'} ) {
131 if ( time - $min_sec > $last ) {
132 my $error = $options{'job'}->update_statustext(
133 int( 100 * $num / scalar( @{$options{'rate_detail'}} ) )
136 $dbh->rollback if $oldAutoCommit;
146 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
155 Delete this record from the database.
159 # the delete method can be inherited from FS::Record
161 =item replace OLD_RECORD [ , OPTION => VALUE ... ]
163 Replaces the OLD_RECORD with this one in the database. If there is an error,
164 returns the error, otherwise returns false.
166 Currently available options are: I<rate_detail>
168 If I<rate_detail> is set to an array reference of FS::rate_detail objects, the
169 objects will have their ratenum field set and will be inserted after this
170 record. Any existing rate_detail records associated with this record will be
176 my ($new, $old) = (shift, shift);
179 local $SIG{HUP} = 'IGNORE';
180 local $SIG{INT} = 'IGNORE';
181 local $SIG{QUIT} = 'IGNORE';
182 local $SIG{TERM} = 'IGNORE';
183 local $SIG{TSTP} = 'IGNORE';
184 local $SIG{PIPE} = 'IGNORE';
186 my $oldAutoCommit = $FS::UID::AutoCommit;
187 local $FS::UID::AutoCommit = 0;
190 # my @old_rate_detail = ();
191 # @old_rate_detail = $old->rate_detail if $options{'rate_detail'};
193 my $error = $new->SUPER::replace($old);
195 $dbh->rollback if $oldAutoCommit;
199 # foreach my $old_rate_detail ( @old_rate_detail ) {
201 # my $error = $old_rate_detail->delete;
203 # $dbh->rollback if $oldAutoCommit;
207 # if ( $options{'job'} ) {
209 # if ( time - $min_sec > $last ) {
210 # my $error = $options{'job'}->update_statustext(
211 # int( 50 * $num / scalar( @old_rate_detail ) )
214 # $dbh->rollback if $oldAutoCommit;
222 if ( $options{'rate_detail'} ) {
223 my $sth = $dbh->prepare('DELETE FROM rate_detail WHERE ratenum = ?') or do {
224 $dbh->rollback if $oldAutoCommit;
228 $sth->execute($old->ratenum) or do {
229 $dbh->rollback if $oldAutoCommit;
233 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
235 foreach my $rate_detail ( @{$options{'rate_detail'}} ) {
237 $rate_detail->ratenum($new->ratenum);
238 $error = $rate_detail->insert;
240 $dbh->rollback if $oldAutoCommit;
244 if ( $options{'job'} ) {
246 if ( time - $min_sec > $last ) {
247 my $error = $options{'job'}->update_statustext(
248 int( 100 * $num / scalar( @{$options{'rate_detail'}} ) )
251 $dbh->rollback if $oldAutoCommit;
262 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
269 Checks all fields to make sure this is a valid rate plan. If there is
270 an error, returns the error, otherwise returns false. Called by the insert
279 $self->ut_numbern('ratenum')
280 || $self->ut_text('ratename')
281 #|| $self->ut_foreign_keyn('agentnum', 'agent', 'agentnum')
282 || $self->ut_numbern('default_detailnum')
284 return $error if $error;
289 =item dest_detail REGIONNUM | RATE_REGION_OBJECTD | HASHREF
291 Returns the rate detail (see L<FS::rate_detail>) for this rate to the
292 specificed destination. If no rate can be found, returns the default
293 rate if there is one, and an empty string otherwise.
295 Destination can be specified as an FS::rate_detail object or regionnum
296 (see L<FS::rate_detail>), or as a hashref containing the following keys:
300 =item I<countrycode> - required.
302 =item I<phonenum> - required.
304 =item I<weektime> - optional. Specifies a time in seconds from the start
305 of the week, and will return a timed rate (one with a non-null I<ratetimenum>)
306 if one exists at that time. If not, returns a non-timed rate.
308 =item I<cdrtypenum> - optional. Specifies a value for the cdrtypenum
309 field, and will return a rate matching that, if one exists. If not, returns
310 a rate with null cdrtypenum.
317 my( $regionnum, $weektime, $cdrtypenum );
318 if ( ref($_[0]) eq 'HASH' ) {
320 my $countrycode = $_[0]->{'countrycode'};
321 my $phonenum = $_[0]->{'phonenum'};
322 $weektime = $_[0]->{'weektime'};
323 $cdrtypenum = $_[0]->{'cdrtypenum'} || '';
325 #find a rate prefix, first look at most specific, then fewer digits,
326 # finally trying the country code only
327 my $rate_prefix = '';
328 $rate_prefix = qsearchs({
329 'table' => 'rate_prefix',
330 'addl_from' => ' JOIN rate_region USING (regionnum)',
332 'countrycode' => $countrycode,
335 'extra_sql' => ' AND exact_match = \'Y\''
338 for my $len ( reverse(1..10) ) {
339 $rate_prefix = qsearchs('rate_prefix', {
340 'countrycode' => $countrycode,
341 #'npa' => { op=> 'LIKE', value=> substr($number, 0, $len) }
342 'npa' => substr($phonenum, 0, $len),
345 $rate_prefix ||= qsearchs('rate_prefix', {
346 'countrycode' => $countrycode,
351 return '' unless $rate_prefix;
353 $regionnum = $rate_prefix->regionnum;
356 $regionnum = ref($_[0]) ? shift->regionnum : shift;
360 'ratenum' => $self->ratenum,
361 'dest_regionnum' => $regionnum,
364 # find all rates matching ratenum, regionnum, cdrtypenum
365 my @details = qsearch( 'rate_detail', {
367 'cdrtypenum' => $cdrtypenum
369 # find all rates maching ratenum, regionnum and null cdrtypenum
370 if ( !@details and $cdrtypenum ) {
371 @details = qsearch( 'rate_detail', {
376 # find one of those matching weektime
377 if ( defined($weektime) ) {
379 my $rate_time = $_->rate_time;
380 $rate_time && $rate_time->contains($weektime)
385 elsif ( @exact > 1 ) {
386 die "overlapping rate_detail times (region $regionnum, time $weektime)\n"
390 # if not found or there is no weektime, find one matching null weektime
392 return $_ if $_->ratetimenum eq '';
394 # if still nothing, return the global default rate for this plan
395 return $self->default_detail;
400 Returns all region-specific details (see L<FS::rate_detail>) for this rate.
406 qsearch( 'rate_detail', { 'ratenum' => $self->ratenum } );
415 eval "use FS::agent";
417 qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
424 Returns the default rate detail, if there is one.
430 $self->default_detailnum ?
431 FS::rate_detail->by_key($self->default_detailnum) : ''
440 Job-queue processor for web interface adds/edits
444 use Storable qw(thaw);
450 my $param = thaw(decode_base64(shift));
451 warn Dumper($param) if $DEBUG;
453 my $old = qsearchs('rate', { 'ratenum' => $param->{'ratenum'} } )
454 if $param->{'ratenum'};
456 my @rate_detail = map {
458 my $regionnum = $_->regionnum;
459 if ( $param->{"sec_granularity$regionnum"} ) {
461 new FS::rate_detail {
462 'dest_regionnum' => $regionnum,
463 map { $_ => $param->{"$_$regionnum"} }
464 qw( min_included min_charge sec_granularity )
465 #qw( min_included conn_charge conn_sec min_charge sec_granularity )
470 new FS::rate_detail {
471 'dest_regionnum' => $regionnum,
477 'sec_granularity' => '60'
482 } qsearch('rate_region', {} );
484 my $rate = new FS::rate {
485 map { $_ => $param->{$_} }
490 if ( $param->{'ratenum'} ) {
491 warn "$rate replacing $old (". $param->{'ratenum'}. ")\n" if $DEBUG;
493 my @param = ( 'job'=>$job );
494 push @param, 'rate_detail'=>\@rate_detail
495 unless $param->{'preserve_rate_detail'};
497 $error = $rate->replace( $old, @param );
500 warn "inserting $rate\n" if $DEBUG;
501 $error = $rate->insert( 'rate_detail' => \@rate_detail,
504 #$ratenum = $rate->getfield('ratenum');
507 die "$error\n" if $error;
515 L<FS::Record>, schema.html from the base documentation.