diff options
author | ivan <ivan> | 2011-11-14 04:31:31 +0000 |
---|---|---|
committer | ivan <ivan> | 2011-11-14 04:31:31 +0000 |
commit | 3db061f538c06804bd0a52b9ef8be3dc6b6db82e (patch) | |
tree | 7335808809768c4dcc6c8c85aebfa3ec365398ea /FS | |
parent | 3892b13ea137969a2b4f880883960e58003cc1b3 (diff) |
rate tiers for vnes, RT#14903
Diffstat (limited to 'FS')
-rw-r--r-- | FS/FS.pm | 4 | ||||
-rw-r--r-- | FS/FS/Schema.pm | 53 | ||||
-rw-r--r-- | FS/FS/cdr.pm | 30 | ||||
-rw-r--r-- | FS/FS/cust_bill_pkg.pm | 23 | ||||
-rw-r--r-- | FS/FS/part_pkg/voip_inbound.pm | 55 | ||||
-rw-r--r-- | FS/FS/part_pkg/voip_tiered.pm | 258 | ||||
-rw-r--r-- | FS/FS/rate_tier.pm | 153 | ||||
-rw-r--r-- | FS/FS/rate_tier_detail.pm | 139 | ||||
-rw-r--r-- | FS/MANIFEST | 4 | ||||
-rw-r--r-- | FS/t/rate_tier.t | 5 | ||||
-rw-r--r-- | FS/t/rate_tier_detail.t | 5 |
11 files changed, 677 insertions, 52 deletions
@@ -250,6 +250,10 @@ L<FS::rate_prefix> - Rate region prefixes for call billing L<FS::rate_detail> - Rate plan detail for call billing +L<FS::rate_tier> - Rate tiers for call billing + +L<FS::rate_tier_details> - Rater tier details for call billing + L<FS::usage_class> - Usage class class L<FS::agent> - Agent (reseller) class diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm index fcdb6542d..50b8b6d5f 100644 --- a/FS/FS/Schema.pm +++ b/FS/FS/Schema.pm @@ -2685,6 +2685,30 @@ sub tables_hashref { 'index' => [], }, + #not really part of the above rate_ stuff (used with flat rate rather than + # rated billing), but could be eventually, and its a rate + 'rate_tier' => { + 'columns' => [ + 'tiernum', 'serial', '', '', '', '', + 'tiername', 'varchar', '', $char_d, '', '', + ], + 'primary_key' => 'tiernum', + 'unique' => [ [ 'tiername'], ], + 'index' => [], + }, + + 'rate_tier_detail' => { + 'columns' => [ + 'tierdetailnum', 'serial', '', '', '', '', + 'tiernum', 'int', '', '', '', '', + 'min_quan', 'int', '', '', '', '', + 'min_charge', 'decimal', '', '10,4', '', '', + ], + 'primary_key' => 'tierdetailnum', + 'unique' => [], + 'index' => [ ['tiernum'], ], + }, + 'usage_class' => { 'columns' => [ 'classnum', 'serial', '', '', '', '', @@ -2872,24 +2896,17 @@ sub tables_hashref { 'max_callers', 'int', 'NULL', '', '', '', ### - # fields for unitel/RSLCOM/convergent that don't map well to asterisk - # defaults - # though these are now used elsewhere: + # old fields for unitel/RSLCOM/convergent that don't map to asterisk + # ones we adoped moved to "own fields" section below # charged_party, upstream_price, rated_price, carrierid, cdrtypenum ### - #cdr_type: Usage = 1, S&E = 7, OC&C = 8 - 'cdrtypenum', 'int', 'NULL', '', '', '', - - 'charged_party', 'varchar', 'NULL', $char_d, '', '', - 'upstream_currency', 'char', 'NULL', 3, '', '', 'upstream_price', 'decimal', 'NULL', '10,4', '', '', 'upstream_rateplanid', 'int', 'NULL', '', '', '', #? # how it was rated internally... 'ratedetailnum', 'int', 'NULL', '', '', '', - 'rated_price', 'decimal', 'NULL', '10,4', '', '', 'distance', 'decimal', 'NULL', '', '', '', 'islocal', 'int', 'NULL', '', '', '', # '', '', 0, '' instead? @@ -2900,16 +2917,24 @@ sub tables_hashref { 'description', 'varchar', 'NULL', $char_d, '', '', 'quantity', 'int', 'NULL', '', '', '', - #cdr_carrier: Telstra =1, Optus = 2, RSL COM = 3 - 'carrierid', 'int', 'NULL', '', '', '', - 'upstream_rateid', 'int', 'NULL', '', '', '', ### #and now for our own fields ### - # a svcnum... right..? + 'cdrtypenum', 'int', 'NULL', '', '', '', + + 'charged_party', 'varchar', 'NULL', $char_d, '', '', + + # how it was rated internally... + 'rated_price', 'decimal', 'NULL', '10,4', '', '', + 'rated_seconds', 'int', 'NULL', '', '', '', + 'rated_minutes', 'double precision', 'NULL', '', '', '', + + 'carrierid', 'int', 'NULL', '', '', '', + + # service it was matched to 'svcnum', 'int', 'NULL', '', '', '', #NULL, done (or something) @@ -2960,6 +2985,8 @@ sub tables_hashref { 'acctid', 'bigint', '', '', '', '', 'termpart', 'int', '', '', '', '',#future use see below 'rated_price', 'decimal', 'NULL', '10,4', '', '', + 'rated_seconds', 'int', 'NULL', '', '', '', + 'rated_minutes', 'double precision', 'NULL', '', '', '', 'status', 'varchar', 'NULL', 32, '', '', 'svcnum', 'int', 'NULL', '', '', '', ], diff --git a/FS/FS/cdr.pm b/FS/FS/cdr.pm index 36721a81a..850f797bc 100644 --- a/FS/FS/cdr.pm +++ b/FS/FS/cdr.pm @@ -401,13 +401,15 @@ error, otherwise returns false. sub set_status_and_rated_price { my($self, $status, $rated_price, $svcnum, %opt) = @_; - if($opt{'inbound'}) { + + if ($opt{'inbound'}) { + my $term = qsearchs('cdr_termination', { acctid => $self->acctid, termpart => 1 # inbound }); my $error; - if($term) { + if ( $term ) { warn "replacing existing cdr status (".$self->acctid.")\n" if $term; $error = $term->delete; return $error if $error; @@ -419,13 +421,19 @@ sub set_status_and_rated_price { status => $status, svcnum => $svcnum, }); + $term->rated_seconds($opt{rated_seconds}) if exists($opt{rated_seconds}); + $term->rated_minutes($opt{rated_minutes}) if exists($opt{rated_minutes}); return $term->insert; - } - else { + + } else { + $self->freesidestatus($status); $self->rated_price($rated_price); + $self->rated_seconds($opt{rated_seconds}) if exists($opt{rated_seconds}); + $self->rated_minutes($opt{rated_minutes}) if exists($opt{rated_minutes}); $self->svcnum($svcnum) if $svcnum; return $self->replace(); + } } @@ -642,6 +650,20 @@ sub export_formats { return %export_formats; } +=item downstream_csv OPTION => VALUE ... + +Options: + +format + +charge + +seconds + +granularity + +=cut + sub downstream_csv { my( $self, %opt ) = @_; diff --git a/FS/FS/cust_bill_pkg.pm b/FS/FS/cust_bill_pkg.pm index ab9d35074..adc09d7a7 100644 --- a/FS/FS/cust_bill_pkg.pm +++ b/FS/FS/cust_bill_pkg.pm @@ -831,11 +831,21 @@ sub usage { if ( $self->get('details') ) { @values = - map { $_->[2] } - grep { ref($_) && ( defined($classnum) ? $_->[3] eq $classnum : 1 ) } + map { ref($_) eq 'HASH' + ? $_->{'amount'} + : $_->[2] + } + grep { ref($_) && ( defined($classnum) + ? $classnum eq ( ref($_) eq 'HASH' + ? $_->{'classnum'} + : $_->[3] + ) + : 1 + ) + } @{ $self->get('details') }; - }else{ + } else { my $hashref = { 'billpkgnum' => $self->billpkgnum }; $hashref->{ 'classnum' } = $classnum if defined($classnum); @@ -863,11 +873,14 @@ sub usage_classes { my %seen = (); foreach my $detail ( grep { ref($_) } @{$self->get('details')} ) { - $seen{ $detail->[3] } = 1; + $seen{ ref($detail) eq 'HASH' + ? $detail->{'classnum'} + : $detail->[3] + } = 1; } keys %seen; - }else{ + } else { map { $_->classnum } qsearch({ table => 'cust_bill_pkg_detail', diff --git a/FS/FS/part_pkg/voip_inbound.pm b/FS/FS/part_pkg/voip_inbound.pm index 425d86bc2..7fb0a5d63 100644 --- a/FS/FS/part_pkg/voip_inbound.pm +++ b/FS/FS/part_pkg/voip_inbound.pm @@ -1,16 +1,15 @@ package FS::part_pkg::voip_inbound; +use base qw( FS::part_pkg::recur_Common ); use strict; -use vars qw(@ISA $DEBUG %info); +use vars qw($DEBUG %info); use Date::Format; use Tie::IxHash; +use Text::CSV_XS; use FS::Conf; use FS::Record qw(qsearchs qsearch); -use FS::part_pkg::recur_Common; use FS::cdr; -use FS::part_pkg::recur_Common; - -@ISA = qw(FS::part_pkg::recur_Common); +use FS::rate_detail; $DEBUG = 0; @@ -56,10 +55,6 @@ tie my %granularity, 'Tie::IxHash', FS::rate_detail::granularities(); 'default' => '+1', }, - 'disable_tollfree' => { 'name' => 'Disable automatic toll-free processing', - 'type' => 'checkbox', - }, - 'use_amaflags' => { 'name' => 'Only charge for CDRs where the amaflags field is set to "2" ("BILL"/"BILLING").', 'type' => 'checkbox', }, @@ -148,7 +143,6 @@ tie my %granularity, 'Tie::IxHash', FS::rate_detail::granularities(); FS::part_pkg::prorate_Mixin::fieldorder, qw( min_charge min_included sec_granularity default_prefix - disable_tollfree use_amaflags use_carrierid use_cdrtypenum ignore_cdrtypenum @@ -160,7 +154,7 @@ tie my %granularity, 'Tie::IxHash', FS::rate_detail::granularities(); bill_every_call ) ], - 'weight' => 40, + 'weight' => 42, ); sub price_info { @@ -205,25 +199,21 @@ sub calc_usage { my $spool_cdr = $cust_pkg->cust_main->spool_cdr; - my $included_min = ($self->option('min_included') - && $self->option('min_included') > 0) - ? $self->option('min_included') : 0; my $charges = 0; # my $downstream_cdr = ''; - my $disable_tollfree = $self->option('disable_tollfree'); - my $ignore_unrateable = $self->option('ignore_unrateable', 'Hush!'); - my $use_duration = $self->option('use_duration'); - - my $output_format = $self->option('output_format', 'Hush!') || 'default'; + my $included_min = $self->option('min_included', 1) || 0; + my $use_duration = $self->option('use_duration'); + my $output_format = $self->option('output_format', 1) || 'default'; + my $granularity = length($self->option('sec_granularity')) + ? $self->option('sec_granularity') + : 60; #for check_chargable, so we don't keep looking up options inside the loop my %opt_cache = (); - eval "use Text::CSV_XS;"; - die $@ if $@; my $csv = new Text::CSV_XS; foreach my $cust_svc ( @@ -232,19 +222,25 @@ sub calc_usage { my $svc_phone = $cust_svc->svc_x; foreach my $cdr ( $svc_phone->get_cdrs( - 'for_update' => 1, - 'status' => '', # unprocessed only - 'default_prefix' => $self->option('default_prefix'), 'inbound' => 1, + 'default_prefix' => $self->option('default_prefix'), + 'status' => '', # unprocessed only + 'for_update' => 1, ) ) { + + my $reason = $self->check_chargable( $cdr, + 'option_cache' => \%opt_cache, + ); + if ( $reason ) { + warn "not charging for CDR ($reason)\n" if $DEBUG; + next; + } + if ( $DEBUG > 1 ) { warn "rating inbound CDR $cdr\n". join('', map { " $_ => ". $cdr->{$_}. "\n" } keys %$cdr ); } - my $granularity = length($self->option('sec_granularity')) - ? $self->option('sec_granularity') - : 60; my $seconds = $use_duration ? $cdr->duration : $cdr->billsec; @@ -287,7 +283,7 @@ sub calc_usage { detail => $call_details[0], amount => $charge, classnum => $cdr->calltypenum, #classnum - phonenum => $self->phonenum, + #phonenum => $self->phonenum, accountcode => $cdr->accountcode, startdate => $cdr->startdate, duration => $seconds, @@ -348,8 +344,7 @@ sub check_chargable { return "carrierid != $opt{'use_carrierid'}" if length($opt{'use_carrierid'}) - && $cdr->carrierid ne $opt{'use_carrierid'} #ne otherwise 0 matches '' - && ! $flags{'da_rewrote'}; + && $cdr->carrierid ne $opt{'use_carrierid'}; #ne otherwise 0 matches '' return "cdrtypenum != $opt{'use_cdrtypenum'}" if length($opt{'use_cdrtypenum'}) diff --git a/FS/FS/part_pkg/voip_tiered.pm b/FS/FS/part_pkg/voip_tiered.pm new file mode 100644 index 000000000..29e60d499 --- /dev/null +++ b/FS/FS/part_pkg/voip_tiered.pm @@ -0,0 +1,258 @@ +package FS::part_pkg::voip_tiered; +use base qw( FS::part_pkg::voip_cdr ); + +use strict; +use vars qw( $DEBUG %info ); +use Tie::IxHash; +use Date::Format; +use Text::CSV_XS; +use FS::Conf; +use FS::Record qw(qsearchs); # qsearch); +use FS::cdr; +use FS::rate_tier; +use FS::rate_detail; + +use Data::Dumper; + +$DEBUG = 0; + +tie my %cdr_inout, 'Tie::IxHash', + 'outbound' => 'Outbound', + 'inbound' => 'Inbound', + 'outbound_inbound' => 'Outbound and Inbound', +; + +tie my %granularity, 'Tie::IxHash', FS::rate_detail::granularities(); + +%info = ( + 'name' => 'VoIP tiered rate pricing of CDRs', + 'shortname' => 'VoIP/telco CDR tiered rating', + 'inherit_fields' => [ 'voip_cdr', 'prorate_Mixin', 'global_Mixin' ], + 'fields' => { + 'tiernum' => { 'name' => 'Tier plan', + 'type' => 'select', + 'select_table' => 'rate_tier', + 'select_key' => 'tiernum', + 'select_label' => 'tiername', + }, + 'cdr_inout' => { 'name'=> 'Call direction when using phone number matching', + 'type'=> 'select', + 'select_options' => \%cdr_inout, + }, + 'min_included' => { 'name' => 'Minutes included', + }, + 'sec_granularity' => { 'name' => 'Granularity', + 'type' => 'select', + 'select_options' => \%granularity, + }, + 'rating_method' => { 'disabled' => 1 }, + 'ratenum' => { 'disabled' => 1 }, + 'intrastate_ratenum' => { 'disabled' => 1 }, + 'min_charge' => { 'disabled' => 1 }, + 'ignore_unrateable' => { 'disabled' => 1 }, + 'domestic_prefix' => { 'disabled' => 1 }, + 'international_prefix' => { 'disabled' => 1 }, + 'disable_tollfree' => { 'disabled' => 1 }, + 'noskip_src_length_accountcode_tollfree' => { 'disabled' => 1 }, + 'accountcode_tollfree_ratenum' => { 'disabled' => 1 }, + 'noskip_dst_length_accountcode_tollfree' => { 'disabled' => 1 }, + }, + 'fieldorder' => [qw( + recur_temporality + recur_method cutoff_day ), + FS::part_pkg::prorate_Mixin::fieldorder, + qw( + cdr_svc_method cdr_inout + tiernum + ) + ], + 'weight' => 44, +); + +sub calc_usage { + my $self = shift; + my($cust_pkg, $sdate, $details, $param ) = @_; + + #my $last_bill = $cust_pkg->last_bill; + my $last_bill = $cust_pkg->get('last_bill'); #->last_bill falls back to setup + + return 0 + if $self->recur_temporality eq 'preceding' + && ( $last_bill eq '' || $last_bill == 0 ); + + my $included_min = $self->option('min_included', 1) || 0; + my $cdr_svc_method = $self->option('cdr_svc_method',1)||'svc_phone.phonenum'; + my $cdr_inout = ($cdr_svc_method eq 'svc_phone.phonenum') + && $self->option('cdr_inout',1) + || 'outbound'; + my $use_duration = $self->option('use_duration'); + my $granularity = length($self->option('sec_granularity')) + ? $self->option('sec_granularity') + : 60; + + #for check_chargable, so we don't keep looking up options inside the loop + my %opt_cache = (); + + my($svc_table, $svc_field) = split('\.', $cdr_svc_method); + + my %options = ( + 'disable_src' => $self->option('disable_src'), + 'default_prefix' => $self->option('default_prefix'), + 'status' => '', + 'for_update' => 1, + ); # $last_bill, $$sdate ) + $options{'by_svcnum'} = 1 if $svc_field eq 'svcnum'; + + ### + # pass one: find the total minutes/calls and store the CDRs + ### + my $total = 0; + my @cdrs = (); + + my @cust_svc; + if( $self->option('bill_inactive_svcs',1) ) { + #XXX in this mode do we need to restrict the set of CDRs by date also? + @cust_svc = $cust_pkg->h_cust_svc($$sdate, $last_bill); + } else { + @cust_svc = $cust_pkg->cust_svc; + } + @cust_svc = grep { $_->part_svc->svcdb eq $svc_table } @cust_svc; + + foreach my $cust_svc (@cust_svc) { + + my $svc_x; + if( $self->option('bill_inactive_svcs',1) ) { + $svc_x = $cust_svc->h_svc_x($$sdate, $last_bill); + } + else { + $svc_x = $cust_svc->svc_x; + } + + foreach my $pass (split('_', $cdr_inout)) { + + $options{'inbound'} = ( $pass eq 'inbound' ); + + foreach my $cdr ( + $svc_x->get_cdrs( %options ) + ) { + if ( $DEBUG > 1 ) { + warn "rating CDR $cdr\n". + join('', map { " $_ => ". $cdr->{$_}. "\n" } keys %$cdr ); + } + + my $charge = ''; + my $seconds = ''; + + $seconds = $use_duration ? $cdr->duration : $cdr->billsec; + + $seconds += $granularity - ( $seconds % $granularity ) + if $seconds # don't granular-ize 0 billsec calls (bills them) + && $granularity # 0 is per call + && $seconds % $granularity; + my $minutes = $granularity ? ($seconds / 60) : 1; + + my $charge_min = $minutes; + + $included_min -= $minutes; + if ( $included_min > 0 ) { + $charge_min = 0; + } else { + $charge_min = 0 - $included_min; + $included_min = 0; + } + + $cdr->tmp_inout( $pass ); + $cdr->tmp_rated_seconds( $seconds ); + $cdr->tmp_rated_minutes( $charge_min ); + $cdr->tmp_svcnum( $cust_svc->svcnum ); + push @cdrs, $cdr; + $total += $charge_min; + + } # $cdr + + } # $pass + + } # $cust_svc + + ### + # pass two: find a tiered rate and do the rest + ### + + my $rate_tier = qsearchs('rate_tier', { tiernum=>$self->option('tiernum') } ) + or die "unknown tiernum ". $self->option('tiernum'); + my $rate_tier_detail = $rate_tier->rate_tier_detail( $total ) + or die "no base rate for tier? ($total)"; + my $min_charge = $rate_tier_detail->min_charge; + + my $output_format = $self->option('output_format', 'Hush!') || 'default'; + + my $csv = new Text::CSV_XS; + + my $charges = 0; + my @invoice_details_sort; + + foreach my $cdr (@cdrs) { + + my $charge_min = $cdr->tmp_rated_minutes; + + my $charge = sprintf('%.4f', ( $min_charge * $charge_min ) + + 0.0000000001 ); #so 1.00005 rounds to 1.0001 + + + if ( $charge > 0 ) { + $charges += $charge; + + my $detail = + $cdr->downstream_csv( 'format' => $output_format, + 'charge' => $charge, + 'seconds' => ($use_duration ? + $cdr->duration : + $cdr->billsec), + 'granularity' => $granularity, + ); + + my $call_details = + { format => 'C', + detail => $detail, + amount => $charge, + #classnum => $cdr->calltypenum, #classnum + #phonenum => $phonenum, #XXX need this to sort on them + accountcode => $cdr->accountcode, + startdate => $cdr->startdate, + duration => $cdr->tmp_rated_seconds, + }; + + #warn " adding details on charge to invoice: [ ". + # join(', ', @{$call_details} ). " ]" + # if ( $DEBUG && ref($call_details) ); + push @invoice_details_sort, [ $call_details, $cdr->calldate_unix ]; + } + + my $error = $cdr->set_status_and_rated_price( + 'done', + $charge, + $cdr->tmp_svcnum, + 'inbound' => ($cdr->tmp_inout eq 'inbound'), + 'rated_minutes' => $charge_min, + 'rated_seconds' => $cdr->tmp_rated_seconds, + ); + die $error if $error; + + + } + + my @sorted_invoice_details = sort { ${$a}[1] <=> ${$b}[1] } @invoice_details_sort; + foreach my $sorted_call_detail ( @sorted_invoice_details ) { + push @$details, ${$sorted_call_detail}[0]; + } + + unshift @$details, { format => 'C', + detail => FS::cdr::invoice_header($output_format), + } + if @$details; + + $charges; +} + +1; + diff --git a/FS/FS/rate_tier.pm b/FS/FS/rate_tier.pm new file mode 100644 index 000000000..1ca457d63 --- /dev/null +++ b/FS/FS/rate_tier.pm @@ -0,0 +1,153 @@ +package FS::rate_tier; +use base qw( FS::o2m_Common FS::Record ); + +use strict; +use FS::Record qw( qsearch qsearchs ); +use FS::rate_tier_detail; + +=head1 NAME + +FS::rate_tier - Object methods for rate_tier records + +=head1 SYNOPSIS + + use FS::rate_tier; + + $record = new FS::rate_tier \%hash; + $record = new FS::rate_tier { 'column' => 'value' }; + + $error = $record->insert; + + $error = $new_record->replace($old_record); + + $error = $record->delete; + + $error = $record->check; + +=head1 DESCRIPTION + +An FS::rate_tier object represents a set of rate tiers. FS::rate_tier inherits + from FS::Record. The following fields are currently supported: + +=over 4 + +=item tiernum + +primary key + +=item tiername + +tiername + + +=back + +=head1 METHODS + +=over 4 + +=item new HASHREF + +Creates a new record. To add the record to the database, see L<"insert">. + +Note that this stores the hash reference, not a distinct copy of the hash it +points to. You can ask the object for a copy with the I<hash> method. + +=cut + +# the new method can be inherited from FS::Record, if a table method is defined + +sub table { 'rate_tier'; } + +=item insert + +Adds this record to the database. If there is an error, returns the error, +otherwise returns false. + +=cut + +# the insert method can be inherited from FS::Record + +=item delete + +Delete this record from the database. + +=cut + +# the delete method can be inherited from FS::Record + +=item replace OLD_RECORD + +Replaces the OLD_RECORD with this one in the database. If there is an error, +returns the error, otherwise returns false. + +=cut + +# the replace method can be inherited from FS::Record + +=item check + +Checks all fields to make sure this is a valid record. If there is +an error, returns the error, otherwise returns false. Called by the insert +and replace methods. + +=cut + +# the check method should currently be supplied - FS::Record contains some +# data checking routines + +sub check { + my $self = shift; + + my $error = + $self->ut_numbern('tiernum') + || $self->ut_text('tiername') + ; + return $error if $error; + + $self->SUPER::check; +} + +=item rate_tier_detail QUANTITY + +=cut + +sub rate_tier_detail { + my $self = shift; + + if ( defined($_[0]) && length($_[0]) ) { + + my $quantity = shift; + + qsearchs({ + 'table' => 'rate_tier_detail', + 'hashref' => { 'tiernum' => $self->tiernum, + 'min_quan' => { op=>'<=', value=>$quantity }, + }, + 'order_by' => 'ORDER BY min_charge ASC LIMIT 1', + }); + + } else { + + qsearch({ + 'table' => 'rate_tier_detail', + 'hashref' => { 'tiernum' => $self->tiernum, }, + 'order_by' => 'ORDER BY min_quan ASC', + }); + + } + +} + +=back + +=head1 BUGS + +=head1 SEE ALSO + +L<FS::Record>, schema.html from the base documentation. + +=cut + +1; + diff --git a/FS/FS/rate_tier_detail.pm b/FS/FS/rate_tier_detail.pm new file mode 100644 index 000000000..60896f489 --- /dev/null +++ b/FS/FS/rate_tier_detail.pm @@ -0,0 +1,139 @@ +package FS::rate_tier_detail; +use base qw( FS::Record ); + +use strict; +use FS::Record; # qw( qsearch qsearchs ); +use FS::rate_tier; + +=head1 NAME + +FS::rate_tier_detail - Object methods for rate_tier_detail records + +=head1 SYNOPSIS + + use FS::rate_tier_detail; + + $record = new FS::rate_tier_detail \%hash; + $record = new FS::rate_tier_detail { 'column' => 'value' }; + + $error = $record->insert; + + $error = $new_record->replace($old_record); + + $error = $record->delete; + + $error = $record->check; + +=head1 DESCRIPTION + +An FS::rate_tier_detail object represents rate tier pricing. +FS::rate_tier_detail inherits from FS::Record. The following fields are +currently supported: + +=over 4 + +=item tierdetailnum + +primary key + +=item tiernum + +tiernum + +=item min_quan + +min_quan + +=item min_charge + +min_charge + + +=back + +=head1 METHODS + +=over 4 + +=item new HASHREF + +Creates a new record. To add the record to the database, see L<"insert">. + +Note that this stores the hash reference, not a distinct copy of the hash it +points to. You can ask the object for a copy with the I<hash> method. + +=cut + +# the new method can be inherited from FS::Record, if a table method is defined + +sub table { 'rate_tier_detail'; } + +=item insert + +Adds this record to the database. If there is an error, returns the error, +otherwise returns false. + +=cut + +# the insert method can be inherited from FS::Record + +=item delete + +Delete this record from the database. + +=cut + +# the delete method can be inherited from FS::Record + +=item replace OLD_RECORD + +Replaces the OLD_RECORD with this one in the database. If there is an error, +returns the error, otherwise returns false. + +=cut + +# the replace method can be inherited from FS::Record + +=item check + +Checks all fields to make sure this is a valid record. If there is +an error, returns the error, otherwise returns false. Called by the insert +and replace methods. + +=cut + +# the check method should currently be supplied - FS::Record contains some +# data checking routines + +sub check { + my $self = shift; + + my $min_quan = $self->min_quan; + $min_quan =~ s/[ ,]//g; + $self->min_quan($min_quan); + + $self->min_quan(0) if $self->min_quan eq ''; + + my $error = + $self->ut_numbern('tierdetailnum') + || $self->ut_foreign_key('tiernum', 'rate_tier', 'tiernum') + || $self->ut_number('min_quan') + || $self->ut_textn('min_charge') #XXX money? but we use 4 decimal places + ; + return $error if $error; + + $self->SUPER::check; +} + +=back + +=head1 BUGS + +=head1 SEE ALSO + +L<FS::Record>, schema.html from the base documentation. + +=cut + +1; + diff --git a/FS/MANIFEST b/FS/MANIFEST index 37f1a5896..e983ea207 100644 --- a/FS/MANIFEST +++ b/FS/MANIFEST @@ -616,3 +616,7 @@ FS/export_nas.pm t/export_nas.t FS/legacy_cust_bill.pm t/legacy_cust_bill.t +FS/rate_tier.pm +t/rate_tier.t +FS/rate_tier_detail.pm +t/rate_tier_detail.t diff --git a/FS/t/rate_tier.t b/FS/t/rate_tier.t new file mode 100644 index 000000000..d735bdbb8 --- /dev/null +++ b/FS/t/rate_tier.t @@ -0,0 +1,5 @@ +BEGIN { $| = 1; print "1..1\n" } +END {print "not ok 1\n" unless $loaded;} +use FS::rate_tier; +$loaded=1; +print "ok 1\n"; diff --git a/FS/t/rate_tier_detail.t b/FS/t/rate_tier_detail.t new file mode 100644 index 000000000..eccd676ae --- /dev/null +++ b/FS/t/rate_tier_detail.t @@ -0,0 +1,5 @@ +BEGIN { $| = 1; print "1..1\n" } +END {print "not ok 1\n" unless $loaded;} +use FS::rate_tier_detail; +$loaded=1; +print "ok 1\n"; |