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;
+use FS::detail_format;
$DEBUG = 0;
%info = (
'name' => 'VoIP flat rate pricing of CDRs for inbound calls',
'shortname' => 'VoIP/telco CDR rating (inbound)',
+ 'inherit_fields' => [ 'prorate_Mixin', 'global_Mixin' ],
'fields' => {
- 'setup_fee' => { 'name' => 'Setup fee for this package',
- 'default' => 0,
- },
- 'recur_fee' => { 'name' => 'Base recurring fee for this package',
- 'default' => 0,
- },
-
#false laziness w/flat.pm
'recur_temporality' => { 'name' => 'Charge recurring fee for period',
'type' => 'select',
'select_options' => \%temporalities,
},
-
- 'unused_credit' => { 'name' => 'Credit the customer for the unused portion'.
- ' of service at cancellation',
- 'type' => 'checkbox',
- },
-
'cutoff_day' => { 'name' => 'Billing Day (1 - 28) for prorating or '.
'subscription',
'default' => '1',
'min_charge' => { 'name' => 'Charge per minute',
},
+
+ 'min_included' => { 'name' => 'Minutes included',
+ },
'sec_granularity' => { 'name' => 'Granularity',
'type' => 'select',
'default' => '+1',
},
- 'disable_tollfree' => { 'name' => 'Disable automatic toll-free processing',
- 'type' => 'checkbox',
- },
-
- 'use_amaflags' => { 'name' => 'Do not charge for CDRs where the amaflags field is not set to "2" ("BILL"/"BILLING").',
+ 'use_amaflags' => { 'name' => 'Only charge for CDRs where the amaflags field is set to "2" ("BILL"/"BILLING").',
'type' => 'checkbox',
},
- 'use_disposition' => { 'name' => 'Do not charge for CDRs where the disposition flag is not set to "ANSWERED".',
- 'type' => 'checkbox',
+ 'use_carrierid' => { 'name' => 'Only charge for CDRs where the Carrier ID is set to any of these (comma-separated) values: ',
},
- 'use_disposition_taqua' => { 'name' => 'Do not charge for CDRs where the disposition is not set to "100" (Taqua).',
- 'type' => 'checkbox',
- },
-
- 'use_carrierid' => { 'name' => 'Do not charge for CDRs where the Carrier ID is not set to: ',
+ 'use_cdrtypenum' => { 'name' => 'Only charge for CDRs where the CDR Type is set to this cdrtypenum: ',
+ },
+
+ 'ignore_cdrtypenum' => { 'name' => 'Do not charge for CDRs where the CDR Type is set to this cdrtypenum: ',
},
- 'use_cdrtypenum' => { 'name' => 'Do not charge for CDRs where the CDR Type is not set to: ',
+ 'use_calltypenum' => { 'name' => 'Only charge for CDRs where the CDR Call Type is set to this cdrtypenum: ',
+ },
+
+ 'ignore_calltypenum' => { 'name' => 'Do not charge for CDRs where the CDR Call Type is set to this cdrtypenum: ',
+ },
+
+ 'ignore_disposition' => { 'name' => 'Do not charge for CDRs where the Disposition is set to any of these (comma-separated) values: ',
+ },
+
+ 'disposition_in' => { 'name' => 'Only charge for CDRs where the Disposition is set to any of these (comma-separated) values: ',
},
'skip_dcontext' => { 'name' => 'Do not charge for CDRs where the dcontext is set to any of these (comma-separated) values:',
},
'fieldorder' => [qw(
- setup_fee recur_fee recur_temporality unused_credit
- recur_method cutoff_day
- min_charge sec_granularity
+ recur_temporality
+ recur_method cutoff_day ),
+ FS::part_pkg::prorate_Mixin::fieldorder,
+ qw( min_charge min_included sec_granularity
default_prefix
- disable_tollfree
- use_amaflags use_disposition
- use_disposition_taqua use_carrierid use_cdrtypenum
+ use_amaflags
+ use_carrierid
+ use_cdrtypenum ignore_cdrtypenum
+ use_calltypenum ignore_calltypenum
+ ignore_disposition disposition_in
skip_dcontext skip_dstchannel_prefix
skip_dst_length_less skip_lastapp
use_duration
bill_every_call
)
],
- 'weight' => 40,
+ 'weight' => 42,
);
-sub calc_setup {
- my($self, $cust_pkg ) = @_;
- $self->option('setup_fee');
+sub price_info {
+ my $self = shift;
+ my $str = $self->SUPER::price_info(@_);
+ $str .= " plus usage" if $str;
+ $str;
}
sub calc_recur {
my $charges = 0;
$charges += $self->calc_usage(@_);
- $charges += $self->calc_recur_Common(@_);
+ $charges += ($cust_pkg->quantity || 1) * $self->calc_recur_Common(@_);
$charges;
}
-sub calc_cancel {
- my $self = shift;
- my($cust_pkg, $sdate, $details, $param ) = @_;
-
- $self->calc_usage(@_);
-}
+# use the default
+#sub calc_cancel {
+# my $self = shift;
+# my($cust_pkg, $sdate, $details, $param ) = @_;
+#
+# $self->calc_usage(@_);
+#}
#false laziness w/voip_sqlradacct calc_recur resolve it if that one ever gets used again
my $last_bill = $cust_pkg->get('last_bill'); #->last_bill falls back to setup
return 0
- if $self->option('recur_temporality', 1) eq 'preceding'
+ if $self->recur_temporality eq 'preceding'
&& ( $last_bill eq '' || $last_bill == 0 );
my $spool_cdr = $cust_pkg->cust_main->spool_cdr;
- my %included_min = ();
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 $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 $output_format = $self->option('output_format', 'Hush!') || 'default';
+ my $formatter =
+ FS::detail_format->new($output_format,
+ buffer => $details,
+ inbound => 1,
+ locale => $cust_pkg->cust_main->locale
+ );
+
+ 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 (
) {
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'),
+ my $cdr_search = $svc_phone->psearch_cdrs(
'inbound' => 1,
- )
- ) {
+ 'default_prefix' => $self->option('default_prefix'),
+ 'status' => '', # unprocessed only
+ 'for_update' => 1,
+ );
+ $cdr_search->limit(1000);
+ $cdr_search->increment(0);
+ while ( my $cdr = $cdr_search->fetch ) {
+
+ my $reason = $self->check_chargable( $cdr,
+ 'option_cache' => \%opt_cache,
+ );
+ if ( $reason ) {
+ warn "not charging for CDR ($reason)\n" if $DEBUG;
+ $cdr_search->adjust(1);
+ 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;
my $minutes = sprintf("%.1f",$seconds / 60);
$minutes =~ s/\.0$// if $granularity == 60; # count whole minutes, convert to integer
$minutes = 1 unless $granularity; # per call
- my $charge = sprintf('%.2f', ( $self->option('min_charge') * $minutes )
+
+ my $charge_min = $minutes;
+ my $charge = 0;
+
+ $included_min -= $minutes;
+ if ( $included_min > 0 ) {
+ $charge_min = 0;
+ }
+ else {
+ $charge_min = 0 - $included_min;
+ $included_min = 0;
+ }
+
+ $charge = sprintf('%.4f', ( $self->option('min_charge') * $charge_min )
+ 0.00000001 ); #so 1.00005 rounds to 1.0001
- next if !$charge;
- $charges += $charge;
- my @call_details = ($cdr->downstream_csv( 'format' => $output_format,
- 'charge' => $charge,
- 'minutes' => $minutes,
- 'granularity' => $granularity,
- )
- );
- push @$details,
- [ 'C',
- $call_details[0],
- $charge,
- $cdr->calltypenum, #classnum
- $self->phonenum,
- $seconds,
- '', #regionname, not set for inbound calls
- ];
-
- my $error = $cdr->set_status_and_rated_price( 'done',
- $charge,
- $cust_svc->svcnum,
- 'inbound' => 1 );
- die $error if $error;
+
+ if ( $charge > 0 ) {
+ $charges += $charge;
+ my @call_details = (
+ $cdr->downstream_csv( 'format' => $output_format,
+ 'charge' => $charge,
+ 'seconds' => ($use_duration
+ ? $cdr->duration
+ : $cdr->billsec
+ ),
+ 'granularity' => $granularity,
+ )
+ );
+# push @$details,
+# { format => 'C',
+# detail => $call_details[0],
+# amount => $charge,
+# classnum => $cdr->calltypenum, #classnum
+# #phonenum => $self->phonenum,
+# accountcode => $cdr->accountcode,
+# startdate => $cdr->startdate,
+# duration => $seconds,
+# # regionname?? => '', #regionname, not set for inbound calls
+# };
+ }
+
+ # eventually use FS::cdr::rate for this
+ my $error = $cdr->set_status_and_rated_price(
+ 'done',
+ $charge,
+ $cust_svc->svcnum,
+ 'rated_seconds' => $use_duration ? $cdr->duration : $cdr->billsec,
+ 'rated_granularity' => $granularity,
+ 'rated_classnum' => $cdr->calltypenum,
+ 'inbound' => 1,
+ );
+ die $error if $error;
+ $formatter->append($cdr);
+
+ $cdr_search->adjust(1) if $cdr->freesidestatus eq '';
} #$cdr
} # $cust_svc
- unshift @$details, [ 'C',
- FS::cdr::invoice_header($output_format),
- '',
- '',
- '',
- '',
- '',
- ]
- if @$details;
+# unshift @$details, { format => 'C',
+# detail => FS::cdr::invoice_header($output_format),
+# }
+# if @$details;
+
+ $formatter->finish;
+ unshift @$details, $formatter->header if @$details;
$charges;
}
#returns a reason why not to rate this CDR, or false if the CDR is chargeable
+# lots of false laziness w/voip_cdr...
sub check_chargable {
my( $self, $cdr, %flags ) = @_;
- #should have some better way of checking these options from a hash
- #or something
-
- my @opt = qw(
- use_amaflags
- use_disposition
- use_disposition_taqua
- use_carrierid
- use_cdrtypenum
- skip_dcontext
- skip_dstchannel_prefix
- skip_dst_length_less
- skip_lastapp
- );
- foreach my $opt (grep !exists($flags{option_cache}->{$_}), @opt ) {
- $flags{option_cache}->{$opt} = $self->option($opt, 1);
- }
- my %opt = %{ $flags{option_cache} };
-
return 'amaflags != 2'
- if $opt{'use_amaflags'} && $cdr->amaflags != 2;
-
- return 'disposition != ANSWERED'
- if $opt{'use_disposition'} && $cdr->disposition ne 'ANSWERED';
-
- return "disposition != 100"
- if $opt{'use_disposition_taqua'} && $cdr->disposition != 100;
-
- return "carrierid != $opt{'use_carrierid'}"
- if length($opt{'use_carrierid'})
- && $cdr->carrierid ne $opt{'use_carrierid'} #ne otherwise 0 matches ''
- && ! $flags{'da_rewrote'};
-
- return "cdrtypenum != $opt{'use_cdrtypenum'}"
- if length($opt{'use_cdrtypenum'})
- && $cdr->cdrtypenum ne $opt{'use_cdrtypenum'}; #ne otherwise 0 matches ''
-
- return "dcontext IN ( $opt{'skip_dcontext'} )"
- if $opt{'skip_dcontext'} =~ /\S/
- && grep { $cdr->dcontext eq $_ } split(/\s*,\s*/, $opt{'skip_dcontext'});
-
- my $len_prefix = length($opt{'skip_dstchannel_prefix'});
- return "dstchannel starts with $opt{'skip_dstchannel_prefix'}"
+ if $self->option_cacheable('use_amaflags') && $cdr->amaflags != 2;
+
+ return "disposition NOT IN ( ". $self->option_cacheable('disposition_in')." )"
+ if $self->option_cacheable('disposition_in') =~ /\S/
+ && !grep { $cdr->disposition eq $_ } split(/\s*,\s*/, $self->option_cacheable('disposition_in'));
+
+ return "disposition IN ( ". $self->option_cacheable('ignore_disposition')." )"
+ if $self->option_cacheable('ignore_disposition') =~ /\S/
+ && grep { $cdr->disposition eq $_ } split(/\s*,\s*/, $self->option_cacheable('ignore_disposition'));
+
+ return "carrierid NOT IN ( ". $self->option_cacheable('use_carrierid'). " )"
+ if $self->option_cacheable('use_carrierid') =~ /\S/
+ && !grep { $cdr->carrierid eq $_ } split(/\s*,\s*/, $self->option_cacheable('use_carrierid')); #eq otherwise 0 matches ''
+
+ # unlike everything else, use_cdrtypenum is applied in FS::svc_x::get_cdrs.
+ return "cdrtypenum != ". $self->option_cacheable('use_cdrtypenum')
+ if length($self->option_cacheable('use_cdrtypenum'))
+ && $cdr->cdrtypenum ne $self->option_cacheable('use_cdrtypenum'); #ne otherwise 0 matches ''
+
+ return "cdrtypenum == ". $self->option_cacheable('ignore_cdrtypenum')
+ if length($self->option_cacheable('ignore_cdrtypenum'))
+ && $cdr->cdrtypenum eq $self->option_cacheable('ignore_cdrtypenum'); #eq otherwise 0 matches ''
+
+ # unlike everything else, use_calltypenum is applied in FS::svc_x::get_cdrs.
+ return "calltypenum != ". $self->option_cacheable('use_calltypenum')
+ if length($self->option_cacheable('use_calltypenum'))
+ && $cdr->calltypenum ne $self->option_cacheable('use_calltypenum'); #ne otherwise 0 matches ''
+
+ return "calltypenum == ". $self->option_cacheable('ignore_calltypenum')
+ if length($self->option_cacheable('ignore_calltypenum'))
+ && $cdr->calltypenum eq $self->option_cacheable('ignore_calltypenum'); #eq otherwise 0 matches ''
+
+ return "dcontext IN ( ". $self->option_cacheable('skip_dcontext'). " )"
+ if $self->option_cacheable('skip_dcontext') =~ /\S/
+ && grep { $cdr->dcontext eq $_ } split(/\s*,\s*/, $self->option_cacheable('skip_dcontext'));
+
+ my $len_prefix = length($self->option_cacheable('skip_dstchannel_prefix'));
+ return "dstchannel starts with ". $self->option_cacheable('skip_dstchannel_prefix')
if $len_prefix
- && substr($cdr->dstchannel,0,$len_prefix) eq $opt{'skip_dstchannel_prefix'};
+ && substr($cdr->dstchannel,0,$len_prefix) eq $self->option_cacheable('skip_dstchannel_prefix');
- my $dst_length = $opt{'skip_dst_length_less'};
+ my $dst_length = $self->option_cacheable('skip_dst_length_less');
return "destination less than $dst_length digits"
if $dst_length && length($cdr->dst) < $dst_length;
- return "lastapp is $opt{'skip_lastapp'}"
- if length($opt{'skip_lastapp'}) && $cdr->lastapp eq $opt{'skip_lastapp'};
+ return "lastapp is ". $self->option_cacheable('skip_lastapp')
+ if length($self->option_cacheable('skip_lastapp')) && $cdr->lastapp eq $self->option_cacheable('skip_lastapp');
#all right then, rate it
'';
# This equates svc_phone records; perhaps svc_phone should have a field
# to indicate it represents a line
+# #XXX no count_available_phones?
sub calc_units {
my($self, $cust_pkg ) = @_;
my $count =