diff --git a/rate-o-mat.pl b/rate-o-mat.pl index c570828..f07832b 100755 --- a/rate-o-mat.pl +++ b/rate-o-mat.pl @@ -8,6 +8,10 @@ use IO::Handle; use Sys::Syslog; use NetAddr::IP; use Data::Dumper; +use Time::HiRes qw(); #for debugging info only +use List::Util qw(shuffle); + +# constants: ########################################################### $0 = 'rate-o-mat'; my $fork = $ENV{RATEOMAT_DAEMONIZE} // 1; @@ -21,10 +25,16 @@ my $log_ident = 'rate-o-mat'; my $log_facility = 'daemon'; my $log_opts = 'ndely,cons,pid,nowait'; -# if split_peak_parts is set to true, rate-o-mat will create a separate -# CDR every time a peak time border is crossed for either the customer, -# the reseller or the carrier billing profile. -my $split_peak_parts = 0; +# number of unrated cdrs to fetch at once: +my $batch_size = $ENV{RATEOMAT_BATCH_SIZE} > 0 ? int $ENV{RATEOMAT_BATCH_SIZE} : 100; + +# if rate-o-mat processes are working on the same accounting.cdr table: +# set to 1 to minimize collisions (and thus rollbacks) +my $shuffle_batch = $ENV{RATEOMAT_SHUFFLE_BATCH} // 0; + +# preload the whole prepaid_costs table, if number of records +# is below this limit: +my $prepaid_costs_cache_limit = $ENV{RATEOMAT_PREPAID_COSTS_CACHE} > 0 ? int $ENV{RATEOMAT_PREPAID_COSTS_CACHE} : 10000; # if the LNP database is used not just for LNP, but also for on-net # billing, special routing or similar things, this should be set to @@ -33,6 +43,23 @@ my $split_peak_parts = 0; # my @lnp_order_by = ("lnp_provider_id ASC"); my @lnp_order_by = (); +# if split_peak_parts is set to true, rate-o-mat will create a separate +# CDR every time a peak time border is crossed for either the customer, +# the reseller or the carrier billing profile. +my $split_peak_parts = $ENV{RATEOMAT_SPLIT_PEAK_PARTS} // 0; + +# update subscriber prepaid attribute value upon profile mapping updates: +my $update_prepaid_preference = 1; + +# terminate if the same cdr fails $failed_cdr_max_retries + 1 times: +my $failed_cdr_max_retries = $ENV{RATEOMAT_MAX_RETRIES} >= 0 ? int $ENV{RATEOMAT_MAX_RETRIES} : 2; +my $failed_cdr_retry_delay = $ENV{RATEOMAT_RETRY_DELAY} >= 0 ? int $ENV{RATEOMAT_RETRY_DELAY} : 30; +# with 2 retries and 30sec delay, rato-o-mat tolerates a replication +# lag of around 60secs until it terminates. + +# pause between db connect attempts: +my $connect_interval = 3; + # billing database my $BillDB_Name = $ENV{RATEOMAT_BILLING_DB_NAME} || 'billing'; my $BillDB_Host = $ENV{RATEOMAT_BILLING_DB_HOST} || 'localhost'; @@ -58,14 +85,24 @@ my $DupDB_Port = $ENV{RATEOMAT_DUPLICATE_DB_PORT} ? int $ENV{RATEOMAT_DUPLICATE_ my $DupDB_User = $ENV{RATEOMAT_DUPLICATE_DB_USER}; my $DupDB_Pass = $ENV{RATEOMAT_DUPLICATE_DB_PASS}; -$split_peak_parts = $ENV{RATEOMAT_SPLIT_PEAK_PARTS} // $split_peak_parts; +my @cdr_fields = qw(source_user_id source_provider_id source_external_subscriber_id source_external_contract_id source_account_id source_user source_domain source_cli source_clir source_ip source_lnp_prefix destination_user_id destination_provider_id destination_external_subscriber_id destination_external_contract_id destination_account_id destination_user destination_domain destination_user_dialed destination_user_in destination_domain_in destination_lnp_prefix peer_auth_user peer_auth_realm call_type call_status call_code init_time start_time duration call_id source_carrier_cost source_reseller_cost source_customer_cost source_carrier_free_time source_reseller_free_time source_customer_free_time source_carrier_billing_fee_id source_reseller_billing_fee_id source_customer_billing_fee_id source_carrier_billing_zone_id source_reseller_billing_zone_id source_customer_billing_zone_id destination_carrier_cost destination_reseller_cost destination_customer_cost destination_carrier_free_time destination_reseller_free_time destination_customer_free_time destination_carrier_billing_fee_id destination_reseller_billing_fee_id destination_customer_billing_fee_id destination_carrier_billing_zone_id destination_reseller_billing_zone_id destination_customer_billing_zone_id frag_carrier_onpeak frag_reseller_onpeak frag_customer_onpeak is_fragmented split rated_at rating_status exported_at export_status); +foreach my $gpp_idx(0 .. 9) { + push @cdr_fields, ("source_gpp$gpp_idx", "destination_gpp$gpp_idx"); +} -######################################################################## +my $cash_balance_col_model_key = 1; +my $time_balance_col_model_key = 2; +my $relation_col_model_key = 3; -sub main; +# globals: ############################################################# my $shutdown = 0; -my $prepaid_costs; +my $prepaid_costs_cache; +my %cdr_col_models = (); +my $rollback; +my $log_fatal = 1; + +# stmt handlers: ####################################################### my $billdbh; my $acctdbh; @@ -82,8 +119,6 @@ my $sth_offpeak_special; my $sth_unrated_cdrs; my $sth_update_cdr; my $sth_create_cdr_fragment; -my $sth_provider_info; -my $sth_reseller_info; my $sth_get_cbalances; my $sth_update_cbalance_w_underrun_profiles_lock; my $sth_update_cbalance_w_underrun_lock; @@ -97,7 +132,9 @@ my $sth_get_first_cbalance; my $sth_get_last_topup_cbalance, my $sth_lnp_number; my $sth_lnp_profile_info; -my $sth_prepaid_costs; +my $sth_prepaid_costs_cache; +my $sth_prepaid_costs_count; +my $sth_prepaid_cost; my $sth_delete_prepaid_cost; my $sth_delete_old_prepaid; my $sth_get_billing_voip_subscribers; @@ -111,73 +148,75 @@ my $sth_update_usr_preference_value; my $sth_delete_usr_preference_value; my $sth_duplicate_cdr; -my $connect_interval = 3; - -my @cdr_fields = qw(source_user_id source_provider_id source_external_subscriber_id source_external_contract_id source_account_id source_user source_domain source_cli source_clir source_ip destination_user_id destination_provider_id destination_external_subscriber_id destination_external_contract_id destination_account_id destination_user destination_domain destination_user_dialed destination_user_in destination_domain_in peer_auth_user peer_auth_realm call_type call_status call_code init_time start_time duration call_id source_carrier_cost source_reseller_cost source_customer_cost source_carrier_free_time source_reseller_free_time source_customer_free_time source_carrier_billing_fee_id source_reseller_billing_fee_id source_customer_billing_fee_id source_carrier_billing_zone_id source_reseller_billing_zone_id source_customer_billing_zone_id destination_carrier_cost destination_reseller_cost destination_customer_cost destination_carrier_free_time destination_reseller_free_time destination_customer_free_time destination_carrier_billing_fee_id destination_reseller_billing_fee_id destination_customer_billing_fee_id destination_carrier_billing_zone_id destination_reseller_billing_zone_id destination_customer_billing_zone_id frag_carrier_onpeak frag_reseller_onpeak frag_customer_onpeak is_fragmented split rated_at rating_status exported_at export_status); -foreach my $gpp_idx(0 .. 9) { - push @cdr_fields, ("source_gpp$gpp_idx", "destination_gpp$gpp_idx"); -} +# run the main loop: ################################################## -main; +main(); exit 0; -######################################################################## +# implementation: ###################################################### -sub FATAL -{ +sub FATAL { my $msg = shift; chomp $msg; print "FATAL: $msg\n" if($fork != 1); - syslog('crit', $msg); + syslog('crit', $msg) if $log_fatal; die "$msg\n"; + } -sub DEBUG -{ +sub DEBUG { + return unless $debug; my $msg = shift; + $msg = &$msg() if 'CODE' eq ref $msg; chomp $msg; $msg =~ s/#012 +/ /g; print "DEBUG: $msg\n" if($fork != 1); syslog('debug', $msg); + } -sub INFO -{ +sub INFO { + my $msg = shift; chomp $msg; print "INFO: $msg\n" if($fork != 1); syslog('info', $msg); + } -sub WARNING -{ +sub WARNING { + my $msg = shift; chomp $msg; print "WARNING: $msg\n" if($fork != 1); syslog('warning', $msg); + } sub sql_time { + my ($time) = @_; my ($y, $m, $d, $H, $M, $S) = (localtime($time))[5,4,3,2,1,0]; $y += 1900; $m += 1; return sprintf('%04d-%02d-%02d %02d:%02d:%02d', $y, $m, $d, $H, $M, $S); + } -sub set_start_strtime -{ +sub set_start_strtime { + my $start = shift; my $r_str = shift; $$r_str = sql_time($start); return 0; + } -sub connect_billdbh -{ +sub connect_billdbh { + do { INFO "Trying to connect to billing db..."; $billdbh = DBI->connect("dbi:mysql:database=$BillDB_Name;host=$BillDB_Host;port=$BillDB_Port", $BillDB_User, $BillDB_Pass, {AutoCommit => 1, mysql_auto_reconnect => 0, mysql_no_autocommit_cmd => 0, PrintError => 1, PrintWarn => 0}); @@ -186,10 +225,11 @@ sub connect_billdbh FATAL "Error connecting to db: ".$DBI::errstr unless defined($billdbh); INFO "Successfully connected to billing db..."; + } -sub connect_acctdbh -{ +sub connect_acctdbh { + do { INFO "Trying to connect to accounting db..."; $acctdbh = DBI->connect("dbi:mysql:database=$AcctDB_Name;host=$AcctDB_Host;port=$AcctDB_Port", $AcctDB_User, $AcctDB_Pass, {AutoCommit => 1, mysql_auto_reconnect => 0, mysql_no_autocommit_cmd => 0, PrintError => 1, PrintWarn => 0}); @@ -198,10 +238,11 @@ sub connect_acctdbh FATAL "Error connecting to db: ".$DBI::errstr unless defined($acctdbh); INFO "Successfully connected to accounting db..."; + } -sub connect_provdbh -{ +sub connect_provdbh { + unless ($ProvDB_User) { undef $dupdbh; WARNING "No provisioning db credentials, disabled."; @@ -216,10 +257,11 @@ sub connect_provdbh FATAL "Error connecting to db: ".$DBI::errstr unless defined($provdbh); INFO "Successfully connected to provisioning db..."; + } -sub connect_dupdbh -{ +sub connect_dupdbh { + unless ($DupDB_User) { undef $dupdbh; WARNING "No duplication db credentials, disabled."; @@ -234,9 +276,11 @@ sub connect_dupdbh FATAL "Error connecting to db: ".$DBI::errstr unless defined($dupdbh); INFO "Successfully connected to duplication db..."; + } sub begin_transaction { + my ($dbh,$isolation_level) = @_; if ($dbh) { if ($isolation_level) { @@ -244,35 +288,54 @@ sub begin_transaction { } $dbh->begin_work or FATAL "Error starting transaction: ".$dbh->errstr; } + } sub commit_transaction { + my $dbh = shift; if ($dbh) { #capture result to force list context and prevent good old komodo perl5db.pl bug: my @wa = $dbh->commit or FATAL "Error committing: ".$dbh->errstr; } + } sub rollback_transaction { + my $dbh = shift; if ($dbh) { my @wa = $dbh->rollback or FATAL "Error rolling back: ".$dbh->errstr; } + +} + +sub rollback_all { + + eval { rollback_transaction($billdbh); }; + eval { rollback_transaction($provdbh); }; + eval { rollback_transaction($acctdbh); }; + eval { rollback_transaction($dupdbh); }; + } sub bigint_to_bytes { + my ($bigint,$size) = @_; return pack('C' x $size, map { hex($_) } (sprintf('%0' . 2 * $size . 's',substr($bigint->as_hex(),2)) =~ /(..)/g)); + } sub is_infinite_unix { + my $unix_ts = shift; return 1 unless defined $unix_ts; #internally, we use undef for infinite future return $unix_ts == 0 ? 1 : 0; #If you pass an out-of-range date to UNIX_TIMESTAMP(), it returns 0 + } sub last_day_of_month { + my $t = shift; my ($month,$year) = (localtime($t))[4,5]; $month++; @@ -298,10 +361,11 @@ sub last_day_of_month { } else { return 30; } + } -sub init_db -{ +sub init_db { + connect_billdbh; connect_provdbh; connect_acctdbh; @@ -356,15 +420,16 @@ sub init_db ) or FATAL "Error preparing get last topup contract balance statement: ".$billdbh->errstr; $sth_get_subscriber_contract_id = $billdbh->prepare( - "SELECT contract_id FROM voip_subscribers WHERE uuid = ?" + "SELECT contract_id FROM billing.voip_subscribers WHERE uuid = ?" ) or FATAL "Error preparing subscriber contract id statement: ".$billdbh->errstr; $sth_billing_info_v4 = $billdbh->prepare(<errstr; $sth_billing_info_v6 = $billdbh->prepare(<errstr; $sth_billing_info_panel = $billdbh->prepare(<= FROM_UNIXTIME(?) ) @@ -409,7 +476,7 @@ EOS $sth_lnp_number = $billdbh->prepare(" SELECT lnp_provider_id - FROM lnp_numbers + FROM billing.lnp_numbers WHERE ? LIKE CONCAT(number,'%') AND (start <= FROM_UNIXTIME(?) OR start IS NULL) AND (end > FROM_UNIXTIME(?) OR end IS NULL) @@ -473,7 +540,7 @@ EOS $sth_unrated_cdrs = $acctdbh->prepare( "SELECT * ". "FROM accounting.cdr WHERE rating_status = 'unrated' ". - "ORDER BY start_time ASC LIMIT 100 " # ."FOR UPDATE" + "ORDER BY start_time ASC LIMIT " . $batch_size ) or FATAL "Error preparing unrated cdr statement: ".$acctdbh->errstr; $sth_update_cdr = $acctdbh->prepare( @@ -489,7 +556,7 @@ EOS "destination_carrier_billing_zone_id = ?, destination_reseller_billing_zone_id = ?, destination_customer_billing_zone_id = ?, ". "frag_carrier_onpeak = ?, frag_reseller_onpeak = ?, frag_customer_onpeak = ?, is_fragmented = ?, ". "duration = ? ". - "WHERE id = ?" + "WHERE id = ? AND rating_status = 'unrated'" ) or FATAL "Error preparing update cdr statement: ".$acctdbh->errstr; if ($split_peak_parts) { @@ -509,30 +576,6 @@ EOS ") or FATAL "Error preparing create cdr fragment statement: ".$acctdbh->errstr; } - $sth_provider_info = $billdbh->prepare( - "SELECT p.class, bm.billing_profile_id ". - "FROM billing.products p, billing.billing_mappings bm ". - "WHERE bm.contract_id = ? AND bm.product_id = p.id ". - "AND (bm.start_date IS NULL OR bm.start_date <= FROM_UNIXTIME(?)) ". - "AND (bm.end_date IS NULL OR bm.end_date >= FROM_UNIXTIME(?)) ". - "ORDER BY bm.start_date DESC, bm.id DESC ". - "LIMIT 1" - ) or FATAL "Error preparing provider info statement: ".$billdbh->errstr; - - $sth_reseller_info = $billdbh->prepare( - "SELECT bm.billing_profile_id, r.contract_id ". - "FROM billing.billing_mappings bm, billing.voip_subscribers vs, ". - "billing.contracts c, billing.contacts ct, billing.resellers r ". - "WHERE vs.uuid = ? AND vs.contract_id = c.id ". - "AND c.contact_id = ct.id ". - "AND ct.reseller_id = r.id ". - "AND r.contract_id = bm.contract_id ". - "AND (bm.start_date IS NULL OR bm.start_date <= FROM_UNIXTIME(?)) ". - "AND (bm.end_date IS NULL OR bm.end_date >= FROM_UNIXTIME(?)) ". - "ORDER BY bm.start_date DESC, bm.id DESC ". - "LIMIT 1" - ) or FATAL "Error preparing reseller info statement: ".$billdbh->errstr; - $sth_get_cbalances = $billdbh->prepare( "SELECT id, cash_balance, cash_balance_interval, ". "free_time_balance, free_time_balance_interval, start, ". @@ -583,16 +626,24 @@ EOS "WHERE id = ?" ) or FATAL "Error preparing update contract balance statement: ".$billdbh->errstr; - $sth_prepaid_costs = $acctdbh->prepare( - "SELECT * FROM prepaid_costs order by timestamp asc" # newer entries overwrite older ones - ) or FATAL "Error preparing prepaid costs statement: ".$acctdbh->errstr; + $sth_prepaid_costs_cache = $acctdbh->prepare( + "SELECT * FROM accounting.prepaid_costs order by timestamp asc" # newer entries overwrite older ones + ) or FATAL "Error preparing prepaid costs cache statement: ".$acctdbh->errstr; - $sth_delete_prepaid_cost = $acctdbh->prepare( - "DELETE FROM prepaid_costs WHERE call_id = ?" + $sth_prepaid_costs_count = $acctdbh->prepare( + "SELECT count(cnt.id) FROM (SELECT id FROM accounting.prepaid_costs LIMIT " . ($prepaid_costs_cache_limit + 1) . ") AS cnt" + ) or FATAL "Error preparing prepaid costs count statement: ".$acctdbh->errstr; + + $sth_prepaid_cost = $acctdbh->prepare( #call_id index required + "SELECT * FROM accounting.prepaid_costs WHERE call_id = ? order by timestamp asc" # newer entries overwrite older ones + ) or FATAL "Error preparing prepaid cost statement: ".$acctdbh->errstr; + + $sth_delete_prepaid_cost = $acctdbh->prepare( #call_id index required + "DELETE FROM accounting.prepaid_costs WHERE call_id = ?" ) or FATAL "Error preparing delete prepaid costs statement: ".$acctdbh->errstr; $sth_delete_old_prepaid = $acctdbh->prepare( - "DELETE FROM prepaid_costs WHERE timestamp < DATE_SUB(NOW(), INTERVAL 7 DAY) LIMIT 10000" + "DELETE FROM accounting.prepaid_costs WHERE timestamp < DATE_SUB(NOW(), INTERVAL 7 DAY) LIMIT 10000" ) or FATAL "Error preparing delete old prepaid statement: ".$acctdbh->errstr; $sth_get_billing_voip_subscribers = $billdbh->prepare( @@ -628,6 +679,78 @@ EOS ) or FATAL "Error preparing delete usr preference value statement: ".$provdbh->errstr; } + prepare_cdr_col_models($cash_balance_col_model_key,'cdr cash balance column model', + [ 'direction', 'provider', 'cash_balance' ], # avoid using Tie::IxHash + { + provider => { + sql => 'SELECT * FROM accounting.cdr_provider', + description => 'get cdr provider cols', + }, + direction => { # the name "direction" for "source" and "destination" is not ideal + sql => 'SELECT * FROM accounting.cdr_direction', + description => 'get cdr direction cols', + }, + cash_balance => { + sql => 'SELECT * FROM accounting.cdr_cash_balance', + description => 'get cdr cash balance cols', + }, + },{ + sql => "INSERT INTO accounting.cdr_cash_balance_data". + " (cdr_id,cdr_start_time,direction_id,provider_id,cash_balance_id,val_before,val_after) VALUES". + " (?,?,?,?,?,?,?) ON DUPLICATE KEY UPDATE ". + "val_before = ?, val_after = ?", + description => 'write cdr cash balance col data', + } + ); + + prepare_cdr_col_models($time_balance_col_model_key,'cdr time balance column model', + [ 'direction', 'provider', 'time_balance' ], + { + provider => { + sql => 'SELECT * FROM accounting.cdr_provider', + description => 'get cdr provider cols', + }, + direction => { + sql => 'SELECT * FROM accounting.cdr_direction', + description => 'get cdr direction cols', + }, + time_balance => { + sql => 'SELECT * FROM accounting.cdr_time_balance', + description => 'get cdr time balance cols', + }, + },{ + sql => "INSERT INTO accounting.cdr_time_balance_data". + " (cdr_id,cdr_start_time,direction_id,provider_id,time_balance_id,val_before,val_after) VALUES". + " (?,?,?,?,?,?,?) ON DUPLICATE KEY UPDATE ". + "val_before = ?, val_after = ?", + description => 'write cdr time balance col data', + } + ); + + prepare_cdr_col_models($relation_col_model_key,'cdr relation column model', + [ 'direction', 'provider', 'relation' ], + { + provider => { + sql => 'SELECT * FROM accounting.cdr_provider', + description => 'get cdr provider cols', + }, + direction => { + sql => 'SELECT * FROM accounting.cdr_direction', + description => 'get cdr direction cols', + }, + relation => { + sql => 'SELECT * FROM accounting.cdr_relation', + description => 'get relation cols', + }, + },{ + sql => "INSERT INTO accounting.cdr_relation_data". + " (cdr_id,cdr_start_time,direction_id,provider_id,relation_id,val) VALUES". + " (?,?,?,?,?,?) ON DUPLICATE KEY UPDATE ". + "val = ?", + description => 'write cdr relation col data', + } + ); + if ($dupdbh) { $sth_duplicate_cdr = $dupdbh->prepare( 'insert into cdr ('. @@ -639,9 +762,11 @@ EOS } return 1; + } sub lock_contracts { + my $cdr = shift; # we lock all contracts when rating a single CDR, which will # eventually need a contract_balances catchup. that are up to 4. @@ -650,12 +775,12 @@ sub lock_contracts { # guaranteed. this final lock statement must avoid joins, otherwise # all rows of joined tables can get locked, since innodb poorly # locks rows by touching an index value. to prepare the lock - # statement, we need to determine the 4 conract ids sperately + # statement, we need to determine the 4 contract ids sperately # before: my %provider_cids = (); - # caller reseller contract: + # caller "provider" contract: $provider_cids{$cdr->{source_provider_id}} = 1 if $cdr->{source_provider_id} ne "0"; - # callee reseller contract: + # callee "provider" contract: $provider_cids{$cdr->{destination_provider_id}} = 1 if $cdr->{destination_provider_id} ne "0"; my @pcids = keys %provider_cids; my $pcid_count = scalar @pcids; @@ -665,7 +790,8 @@ sub lock_contracts { $sth = $billdbh->prepare("SELECT c.id from billing.contracts c ". "WHERE c.id IN (" . substr(',?' x $pcid_count,1) . ")") or FATAL "Error preparing contract row lock selection statement: ".$billdbh->errstr; - $sth->execute(@pcids); + $sth->execute(@pcids) + or FATAL "Error executing contract row lock selection statement: ".$sth->errstr; while (my @res = $sth->fetchrow_array) { $lock_cids{$res[0]} = 1; } @@ -683,7 +809,8 @@ sub lock_contracts { " JOIN billing.voip_subscribers s ON c.id = s.contract_id ". "WHERE s.uuid IN (" . substr(',?' x $uuid_count,1) . ")") or FATAL "Error preparing subscriber contract row lock selection statement: ".$billdbh->errstr; - $sth->execute(@uuids); + $sth->execute(@uuids) + or FATAL "Error executing subscriber contract row lock selection statement: ".$sth->errstr; while (my @res = $sth->fetchrow_array) { $lock_cids{$res[0]} = 1; } @@ -696,15 +823,19 @@ sub lock_contracts { my $sth = $billdbh->prepare("SELECT c.id from billing.contracts c ". "WHERE c.id IN (" . substr(',?' x $lock_count,1) . ") FOR UPDATE") or FATAL "Error preparing contract row lock statement: ".$billdbh->errstr; - $sth->execute(@cids); #finally lock the contract rows at this point + #finally lock the contract rows at this point: + $sth->execute(@cids) + or FATAL "Error executing contract row lock statement: ".$sth->errstr; $sth->finish; DEBUG "$lock_count contract(s) locked: ".join(', ',@cids); } return $lock_count; + } sub add_interval { + my ($unit,$count,$from_time,$align_eom_time,$src) = @_; my $to_time; my ($from_year,$from_month,$from_day,$from_hour,$from_minute,$from_second) = (localtime($from_time))[5,4,3,2,1,0]; @@ -734,12 +865,15 @@ sub add_interval { die("Invalid interval unit '$unit' in $src"); } return $to_time; + } sub truncate_day { + my $t = shift; my ($year,$month,$day,$hour,$minute,$second) = (localtime($t))[5,4,3,2,1,0]; return mktime(0,0,0,$day,$month,$year); + } sub set_subscriber_first_int_attribute_value { @@ -747,6 +881,7 @@ sub set_subscriber_first_int_attribute_value { my $contract_id = shift; my $new_value = shift; my $attribute = shift; + my $readonly = shift; my $changed = 0; my $attr_id = undef; @@ -788,25 +923,37 @@ sub set_subscriber_first_int_attribute_value { $sth->finish; undef $sth; if (defined $val_id) { - if ($new_value == 0) { - $sth = $sth_delete_usr_preference_value; - $sth->execute($val_id) - or FATAL "Error executing delete '$attribute' usr preference value statement: ".$sth->errstr; - $changed++; - DEBUG "'$attribute' usr preference value ID $val_id with value '$old_value' deleted"; + if ($readonly) { + if ($old_value != $new_value) { + WARNING "'$attribute' usr preference value ID $val_id should be '$new_value' instead of '$old_value'"; + } else { + DEBUG "'$attribute' usr preference value ID $val_id value '$new_value' is correct"; + } } else { - $sth = $sth_update_usr_preference_value; - $sth->execute($new_value,$val_id) - or FATAL "Error executing update usr preference value statement: ".$sth->errstr; - $changed++; - DEBUG "'$attribute' usr preference value ID $val_id updated from old value '$old_value' to new value '$new_value'"; + if ($new_value == 0) { + $sth = $sth_delete_usr_preference_value; + $sth->execute($val_id) + or FATAL "Error executing delete '$attribute' usr preference value statement: ".$sth->errstr; + $changed++; + DEBUG "'$attribute' usr preference value ID $val_id with value '$old_value' deleted"; + } else { + $sth = $sth_update_usr_preference_value; + $sth->execute($new_value,$val_id) + or FATAL "Error executing update usr preference value statement: ".$sth->errstr; + $changed++; + DEBUG "'$attribute' usr preference value ID $val_id updated from old value '$old_value' to new value '$new_value'"; + } } } elsif ($new_value > 0) { - $sth = $sth_create_usr_preference_value; - $sth->execute($prov_subs_id,$attr_id,$new_value) - or FATAL "Error executing create usr preference value statement: ".$sth->errstr; - $changed++; - DEBUG "'$attribute' usr preference value ID ".$provdbh->{'mysql_insertid'}." with value '$new_value' created"; + if ($readonly) { + WARNING "'$attribute' usr preference value '$new_value' missing for prov subscriber ID $prov_subs_id"; + } else { + $sth = $sth_create_usr_preference_value; + $sth->execute($prov_subs_id,$attr_id,$new_value) + or FATAL "Error executing create usr preference value statement: ".$sth->errstr; + $changed++; + DEBUG "'$attribute' usr preference value ID ".$provdbh->{'mysql_insertid'}." with value '$new_value' created"; + } } else { DEBUG "'$attribute' usr preference value does not exists and no value is to be set"; } @@ -824,8 +971,9 @@ sub set_subscriber_lock_level { my $contract_id = shift; my $lock_level = shift; #int + my $readonly = shift; - return set_subscriber_first_int_attribute_value($contract_id,$lock_level // 0,'lock'); + return set_subscriber_first_int_attribute_value($contract_id,$lock_level // 0,'lock',$readonly); } @@ -833,8 +981,9 @@ sub switch_prepaid { my $contract_id = shift; my $prepaid = shift; #int + my $readonly = shift; - return set_subscriber_first_int_attribute_value($contract_id,($prepaid ? 1 : 0),'prepaid'); + return set_subscriber_first_int_attribute_value($contract_id,($prepaid ? 1 : 0),'prepaid',$readonly); } @@ -844,6 +993,7 @@ sub add_profile_mappings { my $stime = shift; my $package_id = shift; my $profiles = shift; + my $readonly = shift; my $mappings_added = 0; my $profile_id; @@ -856,22 +1006,26 @@ sub add_profile_mappings { while (my @res = $sth_get_package_profile_sets->fetchrow_array) { ($profile_id,$network_id) = @res; - unless (defined $profile) { - $profile = {}; - get_billing_info($now, $contract_id, undef, $profile) or - FATAL "Error getting billing info for date '".$now."' and contract_id $contract_id\n"; + if ($readonly) { + DEBUG "Adding profile mappings skipped"; + } else { + unless (defined $profile) { + $profile = {}; + get_billing_info($now, $contract_id, undef, $profile) or + FATAL "Error getting billing info for date '".$now."' and contract_id $contract_id\n"; + } + $sth_create_billing_mappings->execute($contract_id,$profile_id,$network_id,$profile->{product_id},$stime) + or FATAL "Error executing create billing mappings statement: ".$sth_create_billing_mappings->errstr; + $sth_create_billing_mappings->finish; + $mappings_added++; } - $sth_create_billing_mappings->execute($contract_id,$profile_id,$network_id,$profile->{product_id},$stime) - or FATAL "Error executing create billing mappings statement: ".$sth_create_billing_mappings->errstr; - $sth_create_billing_mappings->finish; - $mappings_added++; } $sth_get_package_profile_sets->finish; - if ($mappings_added > 0) { + if ($update_prepaid_preference && $mappings_added > 0) { DEBUG "$mappings_added '$profiles' profile mappings added"; get_billing_info($now, $contract_id, undef, $profile) or FATAL "Error getting billing info for date '".$now."' and contract_id $contract_id\n"; - switch_prepaid($contract_id,$profile->{prepaid}); + switch_prepaid($contract_id,$profile->{prepaid},$readonly); } return $mappings_added; @@ -1037,12 +1191,17 @@ PREPARE_BALANCE_CATCHUP: $create_time_aligned = $create_time if $create_time_aligned < $stime; $ratio = ($last_end + 1 - $create_time_aligned) / ($last_end + 1 - $last_start); } - $last_free_balance_int = $last_profile->{int_free_cash} // 0.0; #backward-defaults - $old_free_cash = $ratio * $last_free_balance_int; + #take the previous interval's (old) free cash, e.g. 5euro: + $last_cash_balance_int = $last_profile->{int_free_cash} // 0.0; #backward-defaults + $old_free_cash = $ratio * $last_cash_balance_int; + #carry over the last cash balance value, e.g. 23euro: $cash_balance = $last_cash_balance; if ($last_cash_balance_int < $old_free_cash) { + # the customer didn't spent all of the the old free cash, but + # only e.g. 2euro overall. to get the raw balance, subtract the + # unused rest of the old free cash, e.g. -3euro. $cash_balance = $cash_balance + $last_cash_balance_int - $old_free_cash; - } + } #the customer spent all free cash #free time corrections can take place here once.. #$last_profile->{int_free_time} ... } else { @@ -1050,18 +1209,18 @@ PREPARE_BALANCE_CATCHUP: } $ratio = 1.0; $free_cash = $ratio * ($profile->{int_free_cash} // 0.0); #backward-defaults - $cash_balance += $free_cash; + $cash_balance += $free_cash; #add new free cash $cash_balance_interval = 0.0; $free_time = $ratio * ($profile->{int_free_time} // 0); - $free_time_balance = $free_time; + $free_time_balance = $free_time; #just set free cash for now $free_time_balance_interval = 0; if (!$underrun_lock_applied && defined $underrun_lock_threshold && $last_cash_balance >= $underrun_lock_threshold && $cash_balance < $underrun_lock_threshold) { $underrun_lock_applied = 1; DEBUG "cash balance was decreased from $last_cash_balance to $cash_balance and dropped below underrun lock threshold $underrun_lock_threshold"; if (defined $underrun_lock_level) { - set_subscriber_lock_level($contract_id,$underrun_lock_level); + set_subscriber_lock_level($contract_id,$underrun_lock_level,0); $underrun_lock_time = $now; } } @@ -1069,7 +1228,7 @@ PREPARE_BALANCE_CATCHUP: if (!$underrun_profiles_applied && defined $underrun_profile_threshold && $last_cash_balance >= $underrun_profile_threshold && $cash_balance < $underrun_profile_threshold) { $underrun_profiles_applied = 1; DEBUG "cash balance was decreased from $last_cash_balance to $cash_balance and dropped below underrun profile threshold $underrun_profile_threshold"; - if (add_profile_mappings($contract_id,$stime,$package_id,'underrun') > 0) { + if (add_profile_mappings($contract_id,$stime,$package_id,'underrun',0) > 0) { $underrun_profiles_time = $now; goto PREPARE_BALANCE_CATCHUP; } @@ -1078,8 +1237,8 @@ PREPARE_BALANCE_CATCHUP: #exec create statement: $sth = (defined $etime ? $sth_new_cbalance : $sth_new_cbalance_infinite_future); ($last_cash_balance,$last_cash_balance_int,$last_free_balance,$last_free_balance_int) = - (sprintf("%.4f",$cash_balance), sprintf("%.4f",$cash_balance_interval), - sprintf("%.0f",$free_time_balance), sprintf("%.0f",$free_time_balance_interval)); + (truncate_cash_balance($cash_balance), truncate_cash_balance($cash_balance_interval), + truncate_free_time_balance($free_time_balance), truncate_free_time_balance($free_time_balance_interval)); my @bind_parms = ($contract_id, $last_cash_balance,$last_cash_balance_int,$last_free_balance,$last_free_balance_int, ((defined $underrun_profiles_time ? $underrun_profiles_time : 0)) x 2,((defined $underrun_lock_time ? $underrun_lock_time : 0)) x 2,$stime); @@ -1105,7 +1264,7 @@ PREPARE_BALANCE_CATCHUP: end_unix => $last_end, }; - DEBUG "contract balance created: ".(Dumper $bal); + DEBUG sub { "contract balance created: ".(Dumper $bal) }; $last_profile = $profile; @@ -1140,7 +1299,7 @@ PREPARE_BALANCE_CATCHUP: $underrun_lock_applied = 1; DEBUG "cash balance was decreased from $last_cash_balance to $cash_balance and dropped below underrun lock threshold $underrun_lock_threshold"; if (defined $underrun_lock_level) { - set_subscriber_lock_level($contract_id,$underrun_lock_level); + set_subscriber_lock_level($contract_id,$underrun_lock_level,0); $bal->{underrun_lock_time} = $now; } } @@ -1148,7 +1307,7 @@ PREPARE_BALANCE_CATCHUP: if (!$underrun_profiles_applied && defined $underrun_profile_threshold && $last_cash_balance >= $underrun_profile_threshold && 0.0 < $underrun_profile_threshold) { $underrun_profiles_applied = 1; DEBUG "cash balance was decreased from $last_cash_balance to $cash_balance and dropped below underrun profile threshold $underrun_profile_threshold"; - if (add_profile_mappings($contract_id,$call_start_time,$package_id,'underrun') > 0) { + if (add_profile_mappings($contract_id,$call_start_time,$package_id,'underrun',0) > 0) { $underrun_profiles_time = $now; $bal->{underrun_profile_time} = $now; } @@ -1171,8 +1330,8 @@ PREPARE_BALANCE_CATCHUP: } -sub get_contract_balances -{ +sub get_contract_balances { + my $cdr = shift; my $contract_id = shift; my $r_package_info = shift; @@ -1189,13 +1348,19 @@ sub get_contract_balances my $res = $sth->fetchall_arrayref({}); $sth->finish; - push(@$r_balances, @$res); + foreach my $bal (@$res) { + # balances savepoint: + $bal->{cash_balance_old} = $bal->{cash_balance}; + $bal->{free_time_balance_old} = $bal->{free_time_balance}; + push(@$r_balances,$bal); + } return scalar @$res; + } -sub update_contract_balance -{ +sub update_contract_balance { + my $r_balances = shift; my $changed = 0; @@ -1227,10 +1392,11 @@ sub update_contract_balance DEBUG $changed . " contract balance row(s) updated"; return 1; + } -sub get_subscriber_contract_id -{ +sub get_subscriber_contract_id { + my $uuid = shift; my $sth = $sth_get_subscriber_contract_id; @@ -1241,10 +1407,11 @@ sub get_subscriber_contract_id FATAL "No contract id found for uuid '$uuid'\n" unless @res; return $res[0]; + } -sub get_billing_info -{ +sub get_billing_info { + my $start = shift; my $contract_id = shift; my $source_ip = shift; @@ -1284,22 +1451,24 @@ sub get_billing_info $r_info->{contract_id} = $contract_id; $r_info->{profile_id} = $res[0]; $r_info->{product_id} = $res[1]; - $r_info->{prepaid} = $res[2]; - $r_info->{int_charge} = $res[3]; - $r_info->{int_free_time} = $res[4]; - $r_info->{int_free_cash} = $res[5]; - $r_info->{int_unit} = $res[6]; - $r_info->{int_count} = $res[7]; + $r_info->{class} = $res[2]; + $r_info->{prepaid} = $res[3]; + $r_info->{int_charge} = $res[4]; + $r_info->{int_free_time} = $res[5]; + $r_info->{int_free_cash} = $res[6]; + $r_info->{int_unit} = $res[7]; + $r_info->{int_count} = $res[8]; - DEBUG "contract ID $contract_id billing mapping is profile id $r_info->{profile_id} for time $start from $label"; + DEBUG "contract ID $contract_id billing mapping ($r_info->{class}) is profile id $r_info->{profile_id} for time $start from $label"; $sth->finish; return 1; + } -sub get_profile_info -{ +sub get_profile_info { + my $bpid = shift; my $type = shift; my $direction = shift; @@ -1355,8 +1524,8 @@ sub get_profile_info return 1; } -sub get_offpeak_weekdays -{ +sub get_offpeak_weekdays { + my $bpid = shift; my $start = shift; my $duration = shift; @@ -1373,8 +1542,7 @@ sub get_offpeak_weekdays $start, $start, $duration ) or FATAL "Error executing weekday offpeak statement: ".$sth->errstr; - while(my @res = $sth->fetchrow_array()) - { + while(my @res = $sth->fetchrow_array()) { my %e = (); $e{weekday} = $res[0]; $e{start} = $res[1]; @@ -1383,10 +1551,11 @@ sub get_offpeak_weekdays } return 1; + } -sub get_offpeak_special -{ +sub get_offpeak_special { + my $bpid = shift; my $start = shift; my $duration = shift; @@ -1410,26 +1579,27 @@ sub get_offpeak_special } return 1; + } -sub is_offpeak_special -{ +sub is_offpeak_special { + my $start = shift; my $offset = shift; my $r_offpeaks = shift; - my $secs = $start + $offset; # we have unix-timestamp as referenec + my $secs = $start + $offset; # we have unix-timestamp as reference - foreach my $r_o(@$r_offpeaks) - { + foreach my $r_o(@$r_offpeaks) { return 1 if($secs >= $r_o->{start} && $secs <= $r_o->{end}); } return 0; + } -sub is_offpeak_weekday -{ +sub is_offpeak_weekday { + my $start = shift; my $offset = shift; my $r_offpeaks = shift; @@ -1440,21 +1610,23 @@ sub is_offpeak_weekday #$H -= 1 if($dst == 1); # regard daylight saving time my $secs = $S + $M * 60 + $H * 3600; # we have seconds since midnight as reference - foreach my $r_o(@$r_offpeaks) - { + foreach my $r_o(@$r_offpeaks) { return 1 if($wd == $r_o->{weekday} && $secs >= $r_o->{start} && $secs <= $r_o->{end}); } return 0; + } sub check_shutdown { + if ($shutdown) { syslog('warning', 'Shutdown detected, aborting work in progress'); return 1; } return 0; + } sub get_unrated_cdrs { @@ -1464,11 +1636,10 @@ sub get_unrated_cdrs { $sth->execute or FATAL "Error executing unrated cdr statement: ".$sth->errstr; - #my @cdrs = (); + my @cdrs = (); while (my $cdr = $sth->fetchrow_hashref()) { - #push(@cdrs,$cdr); - push(@$r_cdrs,$cdr); + push(@cdrs,$cdr); check_shutdown() and return 0; } @@ -1479,16 +1650,23 @@ sub get_unrated_cdrs { if $sth->err; $sth->finish; - #sort by end time: - #foreach my $cdr (sort {($a->{start_time} + $a->{duration}) <=> ($b->{start_time} + $b->{duration})} @cdrs) { - # push(@$r_cdrs,$cdr); - #} + if ($shuffle_batch) { + # if concurrent rate-o-mat instances grab the same cdr batch, there + # can be a contention due to waits on same caller/callee contract + # lock attempts when they start processing the batch in the same order. + foreach my $cdr (shuffle @cdrs) { + push(@$r_cdrs,$cdr); + } + } else { + @$r_cdrs = @cdrs; + } return 1; + } -sub update_cdr -{ +sub update_cdr { + my $cdr = shift; $cdr->{rating_status} = 'ok'; @@ -1510,61 +1688,51 @@ sub update_cdr $cdr->{id}) or FATAL "Error executing update cdr statement: ".$sth->errstr; - if ($sth_duplicate_cdr) { - $sth_duplicate_cdr->execute(@$cdr{@cdr_fields}) - or FATAL "Error executing duplicate cdr statement: ".$sth_duplicate_cdr->errstr; + if ($sth->rows > 0) { + DEBUG "cdr ID $cdr->{id} updated"; + if ($sth_duplicate_cdr) { + $sth_duplicate_cdr->execute(@$cdr{@cdr_fields}) + or FATAL "Error executing duplicate cdr statement: ".$sth_duplicate_cdr->errstr; + # todo: new eav fields are not duplicated + } + foreach my $dir (('source', 'destination')) { + foreach my $provider (('carrier','reseller','customer')) { + write_cdr_col_data($cash_balance_col_model_key,$cdr, + { direction => $dir, provider => $provider, cash_balance => 'cash_balance' }, + $cdr->{$dir.'_'.$provider."_cash_balance_before"}, + $cdr->{$dir.'_'.$provider."_cash_balance_after"}); + + write_cdr_col_data($time_balance_col_model_key,$cdr, + { direction => $dir, provider => $provider, time_balance => 'free_time_balance' }, + $cdr->{$dir.'_'.$provider."_free_time_balance_before"}, + $cdr->{$dir.'_'.$provider."_free_time_balance_after"}); + + write_cdr_col_data($relation_col_model_key,$cdr, + { direction => $dir, provider => $provider, relation => 'profile_package_id' }, + $cdr->{$dir.'_'.$provider."_profile_package_id"}); + + write_cdr_col_data($relation_col_model_key,$cdr, + { direction => $dir, provider => $provider, relation => 'contract_balance_id' }, + $cdr->{$dir.'_'.$provider."_contract_balance_id"}); + } + } + } else { + $rollback = 1; + FATAL "cdr ID $cdr->{id} seems to be already processed by someone else"; } return 1; -} -sub get_provider_info -{ - my $pid = shift; - my $start = shift; - my $r_info = shift; - - my $sth = $sth_provider_info; - $sth->execute($pid, $start, $start) - or FATAL "Error executing provider info statement: ".$sth->errstr; - my @res = $sth->fetchrow_array(); - FATAL "No provider info for provider id $pid found\n" - unless(@res); - - $r_info->{class} = $res[0]; - $r_info->{profile_id} = $res[1]; - $r_info->{contract_id} = $pid; - - return 1; } -sub get_reseller_info -{ - my $uuid = shift; - my $start = shift; - my $r_info = shift; - - my $sth = $sth_reseller_info; - $sth->execute($uuid, $start, $start) - or FATAL "Error executing reseller info statement: ".$sth->errstr; - my @res = $sth->fetchrow_array(); - FATAL "No reseller info for user id $uuid found\n" - unless(@res); +sub get_call_cost { - $r_info->{profile_id} = $res[0]; - $r_info->{class} = 'reseller'; - $r_info->{contract_id} = $res[1]; - - return 1; -} - -sub get_call_cost -{ my $cdr = shift; my $type = shift; my $direction = shift; my $contract_id = shift; my $profile_id = shift; + my $readonly = shift; my $r_profile_info = shift; my $r_package_info = shift; my $r_cost = shift; @@ -1579,17 +1747,15 @@ sub get_call_cost my $dst_user = $cdr->{destination_user_in}; my $dst_user_domain = $cdr->{destination_user_in}.'@'.$cdr->{destination_domain}; - DEBUG "fetching call cost for profile_id $profile_id with type $type, direction $direction, ". + DEBUG "calculating call cost for profile_id $profile_id with type $type, direction $direction, ". "src_user_domain $src_user_domain, dst_user_domain $dst_user_domain"; unless(get_profile_info($profile_id, $type, $direction, $src_user_domain, $dst_user_domain, - $r_profile_info, $cdr->{start_time})) - { + $r_profile_info, $cdr->{start_time})) { DEBUG "no match for full uris, trying user only for profile_id $profile_id with type $type, direction $direction, ". "src_user_domain $src_user, dst_user_domain $dst_user"; unless(get_profile_info($profile_id, $type, $direction, $src_user, $dst_user, - $r_profile_info, $cdr->{start_time})) - { + $r_profile_info, $cdr->{start_time})) { # we gracefully ignore missing profile infos for inbound direction FATAL "No outbound fee info for profile $profile_id and ". "source user '$src_user' or user/domain '$src_user_domain' and ". @@ -1603,9 +1769,7 @@ sub get_call_cost $$r_rating_duration = 0; # ensure we start with zero length - DEBUG "billing fee is ".(Dumper $r_profile_info); - - #print Dumper $r_profile_info; + DEBUG sub { "billing fee is ".(Dumper $r_profile_info) }; my @offpeak_weekdays = (); get_offpeak_weekdays($profile_id, $cdr->{start_time}, @@ -1639,45 +1803,35 @@ sub get_call_cost if($duration == 0) { # zero duration call, yes these are possible if(is_offpeak_special($cdr->{start_time}, $offset, \@offpeak_special) - or is_offpeak_weekday($cdr->{start_time}, $offset, \@offpeak_weekdays)) - { + or is_offpeak_weekday($cdr->{start_time}, $offset, \@offpeak_weekdays)) { $$r_onpeak = 0; } else { $$r_onpeak = 1; } } - while($duration > 0) - { + while ($duration > 0) { DEBUG "try to rate remaining duration of $duration secs"; - if(is_offpeak_special($cdr->{start_time}, $offset, \@offpeak_special)) - { + if(is_offpeak_special($cdr->{start_time}, $offset, \@offpeak_special)) { #print "offset $offset is offpeak-special\n"; $onpeak = 0; - } - elsif(is_offpeak_weekday($cdr->{start_time}, $offset, \@offpeak_weekdays)) - { + } elsif(is_offpeak_weekday($cdr->{start_time}, $offset, \@offpeak_weekdays)) { #print "offset $offset is offpeak-weekday\n"; $onpeak = 0; - } - else - { + } else { #print "offset $offset is onpeak\n"; $onpeak = 1; } - unless($init) - { + unless($init) { $init = 1; $interval = $onpeak == 1 ? $r_profile_info->{on_init_interval} : $r_profile_info->{off_init_interval}; $rate = $onpeak == 1 ? $r_profile_info->{on_init_rate} : $r_profile_info->{off_init_rate}; DEBUG "add init rate $rate per sec to costs"; - } - else - { + } else { last if $split_peak_parts and defined($$r_onpeak) and $$r_onpeak != $onpeak and !defined $cdr->{rating_duration}; @@ -1702,13 +1856,13 @@ sub get_call_cost foreach my $bal (@bals) { delete $bal_map{$bal->{id}}; } - @bals = sort {$a->{start_unix} <=> $b->{start_unix}} @bals; + @bals = @{ sort_contract_balances(\@bals) }; my $bal = $bals[0]; $last_bal = $bal; if (defined $prev_bal_id) { if ($bal->{id} != $prev_bal_id) { #contract balance transition - DEBUG "next contract balance entered: ".(Dumper $bal); + DEBUG sub { "next contract balance entered: ".(Dumper $bal) }; $prev_cash_balance = $bal->{cash_balance}; #carry over the costs so far: $cash_balance_rate_sum = 0; @@ -1722,7 +1876,7 @@ sub get_call_cost $prev_bal_id = $bal->{id}; } } else { - DEBUG "starting with contract balance: ".(Dumper $bal); + DEBUG sub { "starting with contract balance: ".(Dumper $bal) }; $prev_bal_id = $bal->{id}; $prev_cash_balance = $bal->{cash_balance}; } @@ -1768,9 +1922,9 @@ sub get_call_cost } if ((scalar @cash_balance_rates) > 0) { - my @remaining_bals = sort {$a->{start_unix} <=> $b->{start_unix}} values %bal_map; + my @remaining_bals = @{ sort_contract_balances([ values %bal_map ]) }; foreach my $bal (@remaining_bals) { - DEBUG "remaining contract balance: ".(Dumper $bal); + DEBUG sub { "remaining contract balance: ".(Dumper $bal) }; $last_bal = $bal; $prev_cash_balance = $bal->{cash_balance}; $cash_balance_rate_sum = 0; @@ -1790,7 +1944,7 @@ sub get_call_cost $underrun_lock_applied = 1; DEBUG "cash balance was decreased from $prev_cash_balance to $last_bal->{cash_balance} and dropped below underrun lock threshold $r_package_info->{underrun_lock_threshold}"; if (defined $r_package_info->{underrun_lock_level}) { - set_subscriber_lock_level($contract_id,$r_package_info->{underrun_lock_level}); + set_subscriber_lock_level($contract_id,$r_package_info->{underrun_lock_level},$readonly); $last_bal->{underrun_lock_time} = $now; } } @@ -1798,17 +1952,229 @@ sub get_call_cost if (!$underrun_profiles_applied && defined $r_package_info->{underrun_profile_threshold} && $prev_cash_balance >= $r_package_info->{underrun_profile_threshold} && $last_bal->{cash_balance} < $r_package_info->{underrun_profile_threshold}) { $underrun_profiles_applied = 1; DEBUG "cash balance was decreased from $prev_cash_balance to $last_bal->{cash_balance} and dropped below underrun profile threshold $r_package_info->{underrun_profile_threshold}"; - if (add_profile_mappings($contract_id,$cdr->{start_time} + $cdr->{duration},$r_package_info->{id},'underrun') > 0) { + if (add_profile_mappings($contract_id,$cdr->{start_time} + $cdr->{duration},$r_package_info->{id},'underrun',$readonly) > 0) { $last_bal->{underrun_profile_time} = $now; } } } return 1; + +} + +sub truncate_cash_balance { + + return sprintf("%.4f",shift); + } +sub truncate_free_time_balance { + + return sprintf("%.0f",shift); + +} + +sub sort_contract_balances { + + my $balances = shift; + my $desc = shift; + $desc = ($desc ? -1 : 1); + my @bals = sort { ($a->{start_unix} <=> $b->{start_unix}) * $desc; } @$balances; + return \@bals; + +} + +sub get_prepaid { + + my $cdr = shift; + my $billing_info = shift; + my $prefix = shift; + my $prepaid = (defined $billing_info ? $billing_info->{prepaid} : undef); + # todo: fetch these from another eav table .. + if (defined $prefix && exists $cdr->{$prefix.'prepaid'} && defined $cdr->{$prefix.'prepaid'}) { + # cdr is supposed to provide prefilled columns: + # source_prepaid + # destination_prepaid <-- mediator should provide this one at least + $prepaid = $cdr->{$prefix.'prepaid'}; + } else { + # undefined without billing info and prefix + } + return $prepaid; + +} + +sub get_snapshot_contract_balance { + + my $balances = shift; + return sort_contract_balances($balances)->[-1]; + +} + +sub populate_prepaid_cost_cache { + + if (!defined $prepaid_costs_cache) { + DEBUG "empty prepaid_costs cache, populate it"; + $sth_prepaid_costs_count->execute() + or FATAL "Error executing get prepaid costs count statement: ".$sth_prepaid_costs_count->errstr; + my ($count) = $sth_prepaid_costs_count->fetchrow_array(); + if ($count > $prepaid_costs_cache_limit) { + WARNING "over $prepaid_costs_cache_limit pending prepaid_costs records, too many to preload"; + } else { + $sth_prepaid_costs_cache->execute() + or FATAL "Error executing get prepaid costs cache statement: ".$sth_prepaid_costs_cache->errstr; + $prepaid_costs_cache = $sth_prepaid_costs_cache->fetchall_hashref('call_id'); + return 1; + } + } else { + DEBUG "prepaid_costs cache already populated"; + } + return 0; + +} + +sub clear_prepaid_cost_cache { + + undef $prepaid_costs_cache; + +} + +sub get_prepaid_cost { + + my $cdr = shift; + my $entry = undef; + if (defined $prepaid_costs_cache) { + if (exists($prepaid_costs_cache->{$cdr->{call_id}})) { + DEBUG "prepaid cost record for call ID $cdr->{call_id} found in cache"; + $entry = $prepaid_costs_cache->{$cdr->{call_id}}; + } + } else { + $sth_prepaid_cost->execute($cdr->{call_id}) + or FATAL "Error executing get prepaid cost statement: ".$sth_prepaid_cost->errstr; + my $prepaid_cost = $sth_prepaid_cost->fetchall_hashref('call_id'); + if ($prepaid_cost && exists($prepaid_cost->{$cdr->{call_id}})) { + DEBUG "prepaid cost record for call ID $cdr->{call_id} fetched"; + $entry = $prepaid_cost->{$cdr->{call_id}}; + } + } + return $entry; + +} + +sub drop_prepaid_cost { + + my $entry = shift; + $sth_delete_prepaid_cost->execute($entry->{call_id}) + or FATAL "Error executing delete prepaid cost statement: ".$sth_delete_prepaid_cost->errstr; + if (defined $prepaid_costs_cache) { + if (delete($prepaid_costs_cache->{$entry->{call_id}})) { + DEBUG "dropped prepaid cost record for call ID $entry->{call_id}"; + } + } + return $sth_delete_prepaid_cost->rows; + +} + +sub prepare_cdr_col_models { + + my $col_model_key = shift; + #print "prepare: $col_model_key\n"; + my $model_description = shift; + my $dimensions = shift; + my $col_dimension_stmt_map = shift; + my $write_stmt = shift; + + $cdr_col_models{$col_model_key} = { description => $model_description, }; + my $model = $cdr_col_models{$col_model_key}; + + $model->{dimensions} = $dimensions; + + my %col_dimension_map = (); + foreach my $dimension (@$dimensions) { + my $stmt = $col_dimension_stmt_map->{$dimension}->{sql}; + my $description = $col_dimension_stmt_map->{$dimension}->{description}; + my $get_col = { description => $description, }; + $get_col->{sth} = $acctdbh->prepare($stmt) + or FATAL "Error preparing $description statement: ".$acctdbh->errstr; + $col_dimension_map{$dimension} = $get_col; + } + $model->{dimension_sths} = \%col_dimension_map; + + $model->{write_sth} = { description => $write_stmt->{description}, }; + $model->{write_sth}->{sth} = $acctdbh->prepare($write_stmt->{sql}) + or FATAL "Error preparing ".$write_stmt->{description}." statement: ".$acctdbh->errstr; + +} + +sub init_cdr_col_model { + + my $col_model_key = shift; + #print "init: $col_model_key\n"; + FATAL "unknown column model key $col_model_key" unless exists $cdr_col_models{$col_model_key}; + my $model = $cdr_col_models{$col_model_key}; + $model->{dimension_dictionaries} = {}; + foreach my $dimension (keys %{$model->{dimension_sths}}) { + my $sth = $model->{dimension_sths}->{$dimension}->{sth}; + $sth->execute() + or FATAL "Error executing ". + $model->{dimension_sths}->{$dimension}->{description} + ." statement: ".$sth->errstr; + $model->{dimension_dictionaries}->{$dimension} = $sth->fetchall_hashref('type'); + $sth->finish; + } + INFO $model->{description} . " loaded\n"; + +} + +sub write_cdr_col_data { + + my $col_model_key = shift; + my $cdr = shift; + my $lookup = shift; + my @vals = @_; + FATAL "unknown column model key $col_model_key" unless exists $cdr_col_models{$col_model_key}; + my $model = $cdr_col_models{$col_model_key}; + my @bind_parms = ($cdr->{id},$cdr->{start_time}); + my $virtual_col_name = ''; + foreach my $dimension (@{$model->{dimensions}}) { + my $dimension_value = $lookup->{$dimension}; + unless ($dimension_value) { + FATAL "missing '$dimension' dimension for writing col data of ".$model->{description}; + } + my $dictionary = $model->{dimension_dictionaries}->{$dimension}; + my $dimension_value_lookup = $dictionary->{$dimension_value}; + unless ($dimension_value_lookup) { + FATAL "unknown '$dimension' col name '$dimension_value' for writing col data of ".$model->{description}; + } + push(@bind_parms,$dimension_value_lookup->{id}); + $virtual_col_name .= '_' if length($virtual_col_name) > 0; + $virtual_col_name .= $lookup->{$dimension}; + } + + if ((scalar @vals) == 0 || (scalar grep { defined $_ } @vals) == 0) { + DEBUG "empty '$virtual_col_name' col data for cdr id ".$cdr->{id}.', skipping'; + return 0; + } else { + push(@bind_parms,@vals); + push(@bind_parms,@vals); + } + + my $sth = $model->{write_sth}->{sth}; + $sth->execute(@bind_parms) + or FATAL "Error executing ". + $model->{write_sth}->{description} + ."statement: ".$sth->errstr; + if ($sth->rows == 1) { + DEBUG 'col data created or up to date for cdr id '.$cdr->{id}.", column '$virtual_col_name': ".join(', ',@vals); + } elsif ($sth->rows > 1) { + DEBUG 'col data updated for cdr id '.$cdr->{id}.", column '$virtual_col_name': ".join(', ',@vals); + #} else { + # DEBUG 'no col data written for cdr id '.$cdr->{id}.", column '$virtual_col_name': ".join(', ',@vals); + } + return $sth->rows; + +} + +sub get_customer_call_cost { -sub get_customer_call_cost -{ my $cdr = shift; my $type = shift; my $direction = shift; @@ -1827,142 +2193,177 @@ sub get_customer_call_cost my $contract_id = get_subscriber_contract_id($cdr->{$dir."user_id"}); - my @balances; + my @balances = (); my %package_info = (); get_contract_balances($cdr, $contract_id, \%package_info, \@balances) - or FATAL "Error getting contract ID $contract_id balances\n"; + or FATAL "Error getting ".$dir."customer contract ID $contract_id balances\n"; my %billing_info = (); #profiles might have switched due to underrun while was carry over discarded get_billing_info($cdr->{start_time}, $contract_id, $cdr->{source_ip}, \%billing_info) or - FATAL "Error getting billing info\n"; - #print Dumper \%billing_info; + FATAL "Error getting ".$dir."customer billing info\n"; + + DEBUG sub { $dir."customer info is " . Dumper({ + billing => \%billing_info, + package => \%package_info, + balances => \@balances, + })}; unless($billing_info{profile_id}) { $$r_rating_duration = $cdr->{duration}; + DEBUG "no billing info for ".$dir."customer contract ID $contract_id, skip"; return -1; } + my $prepaid = get_prepaid($cdr, \%billing_info, $dir.'user_'); + my $outgoing_prepaid = ($prepaid == 1 && $direction eq "out"); + my $prepaid_cost_entry = undef; + if ($outgoing_prepaid) { + DEBUG "billing profile is prepaid"; + populate_prepaid_cost_cache(); + $prepaid_cost_entry = get_prepaid_cost($cdr); + } + my %profile_info = (); get_call_cost($cdr, $type, $direction,$contract_id, - $billing_info{profile_id}, \%profile_info, \%package_info, $r_cost, \$real_cost, $r_free_time, + $billing_info{profile_id}, $outgoing_prepaid && defined $prepaid_cost_entry, + \%profile_info, \%package_info, $r_cost, \$real_cost, $r_free_time, $r_rating_duration, \$onpeak, \@balances) - or FATAL "Error getting customer call cost\n"; + or FATAL "Error getting ".$dir."customer call cost\n"; + + DEBUG "got call cost $$r_cost and free time $$r_free_time"; + + my $snapshot_bal = get_snapshot_contract_balance(\@balances); + $cdr->{$dir."customer_cash_balance_before"} = $snapshot_bal->{cash_balance_old}; + $cdr->{$dir."customer_free_time_balance_before"} = $snapshot_bal->{free_time_balance_old}; + $cdr->{$dir."customer_cash_balance_after"} = $snapshot_bal->{cash_balance_old}; + $cdr->{$dir."customer_free_time_balance_after"} = $snapshot_bal->{free_time_balance_old}; + $cdr->{$dir."customer_profile_package_id"} = $package_info{id}; + $cdr->{$dir."customer_contract_balance_id"} = $snapshot_bal->{id}; $cdr->{$dir."customer_billing_fee_id"} = $profile_info{fee_id}; $cdr->{$dir."customer_billing_zone_id"} = $profile_info{zone_id}; $cdr->{frag_customer_onpeak} = $onpeak if $split_peak_parts; - DEBUG "got call cost $$r_cost and free time $$r_free_time"; - # we don't do prepaid for termination fees for now, so treat it as post-paid - if($billing_info{prepaid} != 1 || $direction eq "in") - { - if($billing_info{prepaid} == 1 && $direction eq "in") { - DEBUG "treat pre-paid billing profile as post-paid for termination fees"; - $$r_cost = $real_cost; - } else { - DEBUG "billing profile is post-paid, update contract balance"; - } - update_contract_balance(\@balances) - or FATAL "Error updating customer contract balance\n"; - } - else { - DEBUG "billing profile is prepaid"; + if ($outgoing_prepaid) { #prepaid out # overwrite the calculated costs with the ones from our table - if (!$prepaid_costs) { - DEBUG "no prepaid_costs, fetch it"; - $sth_prepaid_costs->execute() - or FATAL "Error executing get prepaid costs statement: ".$sth_prepaid_costs->errstr; - $prepaid_costs = $sth_prepaid_costs->fetchall_hashref('call_id'); - } else { - DEBUG "already prefetched prepaid_costs"; - } - if (exists($prepaid_costs->{$cdr->{call_id}})) { - my $entry = $prepaid_costs->{$cdr->{call_id}}; - $$r_cost = $entry->{cost}; - $$r_free_time = $entry->{free_time_used}; - $sth_delete_prepaid_cost->execute($entry->{call_id}); - delete($prepaid_costs->{$cdr->{call_id}}); + if (defined $prepaid_cost_entry) { + $$r_cost = $prepaid_cost_entry->{cost}; + $$r_free_time = $prepaid_cost_entry->{free_time_used}; + drop_prepaid_cost($prepaid_cost_entry); + + # it would be more safe to add *_balance_before/after columns to the prepaid_costs table, + # instead of reconstructing the balance values: + $cdr->{$dir."customer_cash_balance_before"} = truncate_cash_balance($cdr->{$dir."customer_cash_balance_before"} * 1.0 + $prepaid_cost_entry->{cost}); + $cdr->{$dir."customer_free_time_balance_before"} = truncate_free_time_balance($cdr->{$dir."customer_free_time_balance_before"} * 1.0 + $prepaid_cost_entry->{free_time_used}); + } else { + # maybe another rateomat was faster and already processed+deleted it? + # in that case we should bail out here. + WARNING "no prepaid cost record found for call ID $cdr->{call_id}, applying calculated costs"; update_contract_balance(\@balances) - or FATAL "Error updating customer contract balance\n"; + or FATAL "Error updating ".$dir."customer contract balance\n"; $$r_cost = $real_cost; + $cdr->{$dir."customer_cash_balance_after"} = $snapshot_bal->{cash_balance}; + $cdr->{$dir."customer_free_time_balance_after"} = $snapshot_bal->{free_time_balance}; } + } else { #postpaid in, postpaid out, prepaid in + # we don't do prepaid for termination fees for now, so treat it as post-paid + if($prepaid == 1 && $direction eq "in") { #prepaid in + DEBUG "treat pre-paid billing profile as post-paid for termination fees"; + $$r_cost = $real_cost; + } else { #postpaid in, postpaid out + DEBUG "billing profile is post-paid, update contract balance"; + } + update_contract_balance(\@balances) + or FATAL "Error updating ".$dir."customer contract balance\n"; + $cdr->{$dir."customer_cash_balance_after"} = $snapshot_bal->{cash_balance}; + $cdr->{$dir."customer_free_time_balance_after"} = $snapshot_bal->{free_time_balance}; } DEBUG "cost for this call is $$r_cost"; return 1; + } -sub get_provider_call_cost -{ +sub get_provider_call_cost { + my $cdr = shift; my $type = shift; my $direction = shift; - my $r_info = shift; + my $provider_info = shift; my $r_cost = shift; my $r_free_time = shift; my $r_rating_duration = shift; my $onpeak; my $real_cost = 0; - my $contract_id = $$r_info{contract_id}; - - my @balances; - my %package_info = (); - get_contract_balances($cdr, $contract_id, \%package_info, \@balances) - or FATAL "Error getting contract ID $contract_id balances\n"; + my $dir; + if($direction eq "out") { + $dir = "source_"; + } else { + $dir = "destination_"; + } - my %billing_info = (); - get_billing_info($cdr->{start_time}, $contract_id, $cdr->{source_ip}, \%billing_info) or - FATAL "Error getting billing info\n"; - #print Dumper \%billing_info; + my $contract_id = $provider_info->{billing}->{contract_id}; - unless($billing_info{profile_id}) { + unless($provider_info->{billing}->{profile_id}) { $$r_rating_duration = $cdr->{duration}; + DEBUG "no billing info for ".$dir."provider contract ID $contract_id, skip"; return -1; } + my $provider_type; + if ($provider_info->{billing}->{class} eq "reseller") { + $provider_type = "reseller_"; + } else { + $provider_type = "carrier_"; + } + + my $prepaid = get_prepaid($cdr, $provider_info->{billing},$dir.'provider_'); + my %profile_info = (); get_call_cost($cdr, $type, $direction,$contract_id, - $r_info->{profile_id}, \%profile_info, \%package_info, $r_cost, \$real_cost, $r_free_time, - $r_rating_duration, \$onpeak, \@balances) - or FATAL "Error getting provider call cost\n"; + $provider_info->{billing}->{profile_id}, $prepaid, # no underruns for providers with prepaid profile + \%profile_info, $provider_info->{package}, $r_cost, \$real_cost, $r_free_time, + $r_rating_duration, \$onpeak, $provider_info->{balances}) + or FATAL "Error getting ".$dir."provider call cost\n"; - unless($billing_info{prepaid} == 1) - { - update_contract_balance(\@balances) - or FATAL "Error updating provider contract balance\n"; - } + my $snapshot_bal = get_snapshot_contract_balance($provider_info->{balances}); - if($r_info->{class} eq "reseller") - { - if($direction eq 'out') { - $cdr->{source_reseller_billing_fee_id} = $profile_info{fee_id}; - $cdr->{source_reseller_billing_zone_id} = $profile_info{zone_id}; - } elsif($direction eq 'in') { - $cdr->{destination_reseller_billing_fee_id} = $profile_info{fee_id}; - $cdr->{destination_reseller_billing_zone_id} = $profile_info{zone_id}; - } - $cdr->{frag_reseller_onpeak} = $onpeak if $split_peak_parts; - } - else - { - if($direction eq 'out') { - $cdr->{source_carrier_billing_fee_id} = $profile_info{fee_id}; - $cdr->{source_carrier_billing_zone_id} = $profile_info{zone_id}; - } elsif($direction eq 'in') { - $cdr->{destination_carrier_billing_fee_id} = $profile_info{fee_id}; - $cdr->{destination_carrier_billing_zone_id} = $profile_info{zone_id}; - } - $cdr->{frag_carrier_onpeak} = $onpeak if $split_peak_parts; + $cdr->{$dir.$provider_type."package_id"} = $provider_info->{package}->{id}; + $cdr->{$dir.$provider_type."contract_balance_id"} = $snapshot_bal->{id}; + + $cdr->{$dir.$provider_type."billing_fee_id"} = $profile_info{fee_id}; + $cdr->{$dir.$provider_type."billing_zone_id"} = $profile_info{zone_id}; + $cdr->{'frag_'.$provider_type.'onpeak'} = $onpeak if $split_peak_parts; + + unless($prepaid == 1) { + $cdr->{$dir.$provider_type."cash_balance_before"} = $snapshot_bal->{cash_balance_old}; + $cdr->{$dir.$provider_type."free_time_balance_before"} = $snapshot_bal->{free_time_balance_old}; + $cdr->{$dir.$provider_type."cash_balance_after"} = $snapshot_bal->{cash_balance_old}; + $cdr->{$dir.$provider_type."free_time_balance_after"} = $snapshot_bal->{free_time_balance_old}; + + update_contract_balance($provider_info->{balances}) + or FATAL "Error updating ".$dir.$provider_type."provider contract balance\n"; + + $cdr->{$dir.$provider_type."cash_balance_after"} = $snapshot_bal->{cash_balance}; + $cdr->{$dir.$provider_type."free_time_balance_after"} = $snapshot_bal->{free_time_balance}; + + } else { + WARNING $dir.$provider_type."provider is prepaid\n"; + # there are no prepaid cost records for providers, so we cannot + # restore the original balance and leave the fields empty + + # no balance update for providers with prepaid profile } return 1; + } -sub rate_cdr -{ +sub rate_cdr { + my $cdr = shift; my $type = shift; @@ -1982,8 +2383,7 @@ sub rate_cdr my $direction; my @rating_durations; - unless($cdr->{call_status} eq "ok") - { + unless($cdr->{call_status} eq "ok") { DEBUG "cdr #$$cdr{id} has call_status $$cdr{call_status}, skip."; $cdr->{source_carrier_cost} = $source_carrier_cost; $cdr->{source_reseller_cost} = $source_reseller_cost; @@ -2001,30 +2401,50 @@ sub rate_cdr } DEBUG "fetching source provider info for source_provider_id #$$cdr{source_provider_id}"; - my %source_provider_info = (); + my %source_provider_billing_info = (); + my %source_provider_package_info = (); + my @source_provider_balances = (); if($cdr->{source_provider_id} eq "0") { WARNING "Missing source_provider_id for source_user_id ".$cdr->{source_user_id}." in cdr #".$cdr->{id}."\n"; } else { - get_provider_info($cdr->{source_provider_id}, $cdr->{start_time}, \%source_provider_info) - or FATAL "Error getting source provider info for cdr #".$cdr->{id}."\n"; + # we have to catchup balances at this point before getting the profile, since underrun profiles could get applied: + get_contract_balances($cdr, $cdr->{source_provider_id}, \%source_provider_package_info, \@source_provider_balances) + or FATAL "Error getting source provider contract ID $cdr->{source_provider_id} balances\n"; + get_billing_info($cdr->{start_time}, $cdr->{source_provider_id}, $cdr->{source_ip}, \%source_provider_billing_info) + or FATAL "Error getting source provider billing info for cdr #".$cdr->{id}."\n"; } - DEBUG "source_provider_info is ".(Dumper \%source_provider_info); - - #unless($source_provider_info{profile_info}) { + my $source_provider_info = { + billing => \%source_provider_billing_info, + package => \%source_provider_package_info, + balances => \@source_provider_balances, + }; + DEBUG sub { "source_provider_info is ".(Dumper $source_provider_info) }; + + #unless($source_provider_billing_info{profile_info}) { # FATAL "Missing billing profile for source_provider_id ".$cdr->{source_provider_id}." for cdr #".$cdr->{id}."\n"; #} DEBUG "fetching destination provider info for destination_provider_id #$$cdr{destination_provider_id}"; - my %destination_provider_info = (); + my %destination_provider_billing_info = (); + my %destination_provider_package_info = (); + my @destination_provider_balances = (); if($cdr->{destination_provider_id} eq "0") { WARNING "Missing destination_provider_id for destination_user_id ".$cdr->{destination_user_id}." in cdr #".$cdr->{id}."\n"; } else { - get_provider_info($cdr->{destination_provider_id}, $cdr->{start_time}, \%destination_provider_info) - or FATAL "Error getting destination provider info for cdr #".$cdr->{id}."\n"; + # we have to catchup balances at this point before getting the profile, since underrun profiles could get applied: + get_contract_balances($cdr, $cdr->{destination_provider_id}, \%destination_provider_package_info, \@destination_provider_balances) + or FATAL "Error getting destination provider contract ID $cdr->{destination_provider_id} balances\n"; + get_billing_info($cdr->{start_time}, $cdr->{destination_provider_id}, $cdr->{source_ip}, \%destination_provider_billing_info) + or FATAL "Error getting destination provider billing info for cdr #".$cdr->{id}."\n"; } - DEBUG "destination_provider_info is ".(Dumper \%destination_provider_info); - - #unless($destination_provider_info{profile_info}) { + my $destination_provider_info = { + billing => \%destination_provider_billing_info, + package => \%destination_provider_package_info, + balances => \@destination_provider_balances, + }; + DEBUG sub { "destination_provider_info is ".(Dumper $destination_provider_info) }; + + #unless($destination_provider_billing_info{profile_info}) { # FATAL "Missing billing profile for destination_provider_id ".$cdr->{destination_provider_id}." for cdr #".$cdr->{id}."\n"; #} @@ -2032,7 +2452,7 @@ sub rate_cdr if($cdr->{source_user_id} ne "0") { DEBUG "call from local subscriber, source_user_id is $$cdr{source_user_id}"; # if we have a call from local subscriber, the source provider MUST be a reseller - if($source_provider_info{profile_id} && $source_provider_info{class} ne "reseller") { + if($source_provider_billing_info{profile_id} && $source_provider_billing_info{class} ne "reseller") { FATAL "The local source_user_id ".$cdr->{source_user_id}." has a source_provider_id ".$cdr->{source_provider_id}. " which is not a reseller in cdr #".$cdr->{id}."\n"; } @@ -2045,10 +2465,10 @@ sub rate_cdr # for calls towards a local user, termination fees might apply if # we find a fee with direction "in" - if($destination_provider_info{profile_id}) { - DEBUG "destination provider has billing profile $destination_provider_info{profile_id}, get reseller termination cost"; + if($destination_provider_billing_info{profile_id}) { + DEBUG "destination provider has billing profile $destination_provider_billing_info{profile_id}, get reseller termination cost"; get_provider_call_cost($cdr, $type, "in", - \%destination_provider_info, \$destination_reseller_cost, \$destination_reseller_free_time, + $destination_provider_info, \$destination_reseller_cost, \$destination_reseller_free_time, \$rating_durations[@rating_durations]) or FATAL "Error getting destination reseller cost for local destination_provider_id ". $cdr->{destination_provider_id}." for cdr ".$cdr->{id}."\n"; @@ -2071,21 +2491,21 @@ sub rate_cdr # for the carrier cost, we use the destination billing profile of a peer # (this is what the peering provider is charging the carrier) - if($destination_provider_info{profile_id}) { - DEBUG "fetching source_carrier_cost based on destination_provider_info ".(Dumper \%destination_provider_info); + if($destination_provider_billing_info{profile_id}) { + DEBUG sub { "fetching source_carrier_cost based on destination_provider_billing_info ".(Dumper \%destination_provider_billing_info) }; get_provider_call_cost($cdr, $type, "out", - \%destination_provider_info, \$source_carrier_cost, \$source_carrier_free_time, + $destination_provider_info, \$source_carrier_cost, \$source_carrier_free_time, \$rating_durations[@rating_durations]) or FATAL "Error getting source carrier cost for cdr ".$cdr->{id}."\n"; } else { - WARNING "missing destination profile, so we can't calculate source_carrier_cost for destination_provider_info ".(Dumper \%destination_provider_info); + WARNING "missing destination profile, so we can't calculate source_carrier_cost for destination_provider_billing_info ".(Dumper \%destination_provider_billing_info); } } # get reseller cost - if($source_provider_info{profile_id}) { + if($source_provider_billing_info{profile_id}) { get_provider_call_cost($cdr, $type, "out", - \%source_provider_info, \$source_reseller_cost, \$source_reseller_free_time, + $source_provider_info, \$source_reseller_cost, \$source_reseller_free_time, \$rating_durations[@rating_durations]) or FATAL "Error getting source reseller cost for cdr ".$cdr->{id}."\n"; } else { @@ -2111,27 +2531,27 @@ sub rate_cdr # we use the source provider info (the one of the peer) for the carrier termination fees, # as this is what the peer is charging us - if($source_provider_info{profile_id}) { - DEBUG "fetching destination_carrier_cost based on source_provider_info ".(Dumper \%source_provider_info); + if($source_provider_billing_info{profile_id}) { + DEBUG sub { "fetching destination_carrier_cost based on source_provider_billing_info ".(Dumper \%source_provider_billing_info) }; get_provider_call_cost($cdr, $type, "in", - \%source_provider_info, \$destination_carrier_cost, \$destination_carrier_free_time, + $source_provider_info, \$destination_carrier_cost, \$destination_carrier_free_time, \$rating_durations[@rating_durations]) or FATAL "Error getting destination carrier cost for local destination_provider_id ". $cdr->{destination_provider_id}." for cdr ".$cdr->{id}."\n"; } else { - WARNING "missing source profile, so we can't calculate destination_carrier_cost for source_provider_info ".(Dumper \%source_provider_info); + WARNING "missing source profile, so we can't calculate destination_carrier_cost for source_provider_billing_info ".(Dumper \%source_provider_billing_info); } - if($destination_provider_info{profile_id}) { - DEBUG "fetching destination_reseller_cost based on source_provider_info ".(Dumper \%destination_provider_info); + if($destination_provider_billing_info{profile_id}) { + DEBUG sub { "fetching destination_reseller_cost based on source_provider_billing_info ".(Dumper \%destination_provider_billing_info) }; get_provider_call_cost($cdr, $type, "in", - \%destination_provider_info, \$destination_reseller_cost, \$destination_reseller_free_time, + $destination_provider_info, \$destination_reseller_cost, \$destination_reseller_free_time, \$rating_durations[@rating_durations]) or FATAL "Error getting destination reseller cost for local destination_provider_id ". $cdr->{destination_provider_id}." for cdr ".$cdr->{id}."\n"; } else { # up to 2.8, there is one hardcoded reseller id 1, which doesn't have a billing profile, so skip this step here. # in theory, all resellers MUST have a billing profile, so we could bail out here - WARNING "missing destination profile, so we can't calculate destination_reseller_cost for destination_provider_info ".(Dumper \%destination_provider_info); + WARNING "missing destination profile, so we can't calculate destination_reseller_cost for destination_provider_billing_info ".(Dumper \%destination_provider_billing_info); } get_customer_call_cost($cdr, $type, "in", \$destination_customer_cost, \$destination_customer_free_time, @@ -2161,6 +2581,13 @@ sub rate_cdr my $sth = $sth_create_cdr_fragment; $sth->execute($rating_duration, $rating_duration, $cdr->{id}) or FATAL "Error executing create cdr fragment statement: ".$sth->errstr; + if ($sth->rows > 0) { + DEBUG "cdr ID $cdr->{id} covers $rating_duration secs before crossing coherent onpeak/offpeak. another cdr for remaining " . + ($cdr->{duration} - $rating_duration) . " secs of call ID $cdr->{call_id} was created"; + } else { + $rollback = 1; + FATAL "cdr ID $cdr->{id} seems to be already processed by someone else"; + } $cdr->{is_fragmented} = 1; $cdr->{duration} = $rating_duration; } @@ -2179,10 +2606,11 @@ sub rate_cdr $cdr->{destination_reseller_free_time} = $destination_reseller_free_time; $cdr->{destination_customer_free_time} = $destination_customer_free_time; return 1; + } -sub daemonize -{ +sub daemonize { + my $pidfile = shift; chdir '/' or FATAL "Can't chdir to /: $!\n"; @@ -2197,16 +2625,26 @@ sub daemonize seek $PID, 0, SEEK_SET; truncate $PID, 0; printflush $PID "$$\n"; + } -sub signal_handler -{ +sub signal_handler { + $shutdown = 1; + +} + +sub debug_rating_time { + + my $t = shift; + my $cdr_id = shift; + my $error = shift; + DEBUG sub { "rating cdr ID $cdr_id " . ($error ? "aborted after" : "completed successfully in") . ' ' . sprintf("%.3f",Time::HiRes::time() - $t) . " secs" }; + } +sub main { -sub main -{ openlog($log_ident, $log_opts, $log_facility) or die "Error opening syslog: $!\n"; @@ -2218,30 +2656,38 @@ sub main init_db or FATAL "Error initializing database handlers\n"; my $rated = 0; my $next_del = 10000; + my %failed_counter_map = (); + foreach (keys %cdr_col_models) { + init_cdr_col_model($_); + } INFO "Up and running.\n"; - while(!$shutdown) - { + while (!$shutdown) { + + $log_fatal = 1; $billdbh->ping || init_db; $acctdbh->ping || init_db; $provdbh and ($provdbh->ping || init_db); $dupdbh and ($dupdbh->ping || init_db); - undef($prepaid_costs); + clear_prepaid_cost_cache(); + my $error; my @cdrs = (); if ($billdbh && $acctdbh && $provdbh) { eval { get_unrated_cdrs(\@cdrs); + INFO "Grabbed ".(scalar @cdrs)." CDRs" if (scalar @cdrs) > 0; }; - if($@) { - if($DBI::err == 2006) { + $error = $@; + if ($error) { + if ($DBI::err == 2006) { INFO "DB connection gone, retrying..."; next; } - FATAL "Error getting next bunch of CDRs: " . $@; + FATAL "Error getting next bunch of CDRs: " . $error; } } else { - WARNING "no-op loop since not all mandatory db connections are available"; + WARNING "no-op loop since mandatory db connections are n/a"; } $shutdown and last; @@ -2253,87 +2699,117 @@ sub main } my $rated_batch = 0; - - eval - { - foreach my $cdr (@cdrs) - { - # required to avoid contract_balances duplications during catchup: - begin_transaction($billdbh,'READ COMMITTED'); - # row locks are released upon commit/rollback and have to cover - # the whole transaction. thus locking contract rows for preventing - # concurrent catchups will be our very first SQL statement in the - # billingdb transaction: - lock_contracts($cdr); - begin_transaction($provdbh); - begin_transaction($acctdbh); - begin_transaction($dupdbh); - - INFO "rate cdr #".$cdr->{id}."\n"; - rate_cdr($cdr, $type) && update_cdr($cdr); - $rated_batch++; - - # we would need a XA/distributed transaction manager for this: - commit_transaction($billdbh); - commit_transaction($provdbh); - commit_transaction($acctdbh); - commit_transaction($dupdbh); - - check_shutdown() and last; + my $t; + my $cdr_id; + my $info_prefix; + my $failed = 0; + eval { + foreach my $cdr (@cdrs) { + $rollback = 0; + $log_fatal = 0; + $info_prefix = ($rated_batch + 1) . "/" . (scalar @cdrs) . " - "; + eval { + $t = Time::HiRes::time() if $debug; + $cdr_id = $cdr->{id}; + DEBUG "start rating CDR ID $cdr_id"; + # required to avoid contract_balances duplications during catchup: + begin_transaction($billdbh,'READ COMMITTED'); + # row locks are released upon commit/rollback and have to cover + # the whole transaction. thus locking contract rows for preventing + # concurrent catchups will be our very first SQL statement in the + # billingdb transaction: + lock_contracts($cdr); + begin_transaction($provdbh); + begin_transaction($acctdbh); + begin_transaction($dupdbh); + + INFO $info_prefix."rate CDR ID ".$cdr->{id}; + rate_cdr($cdr, $type) && update_cdr($cdr); + + # we would need a XA/distributed transaction manager for this: + commit_transaction($billdbh); + commit_transaction($provdbh); + commit_transaction($acctdbh); + commit_transaction($dupdbh); + + $rated_batch++; + delete $failed_counter_map{$cdr_id}; + debug_rating_time($t,$cdr_id,0); + check_shutdown() and last; + }; + $error = $@; + if ($error) { + debug_rating_time($t,$cdr_id,1); + if ($rollback) { + INFO $info_prefix."rolling back changes for CDR ID $cdr_id"; + rollback_all(); + next; #move on to the next cdr of the batch + } else { + $failed_counter_map{$cdr_id} = 0 if !exists $failed_counter_map{$cdr_id}; + if ($failed_counter_map{$cdr_id} < $failed_cdr_max_retries && !defined $DBI::err) { + WARNING $info_prefix."rating CDR ID $cdr_id aborted " . + ($failed_counter_map{$cdr_id} > 0 ? " (retry $failed_counter_map{$cdr_id})" : "") . + ": " . $error; + $failed_counter_map{$cdr_id} = $failed_counter_map{$cdr_id} + 1; + $failed += 1; + next; #move on to the next cdr of the batch + } else { + die($error); #rethrow + } + } + } } }; - if($@) - { - my $error = $@; - if(defined $DBI::err) - { + $log_fatal = 1; + $error = $@; + if ($error) { + if (defined $DBI::err) { INFO "Caught DBI:err ".$DBI::err, "\n"; - if($DBI::err == 2006) - { + if ($DBI::err == 2006) { INFO "DB connection gone, retrying..."; # disconnect from all of them so transactions are on par - eval { rollback_transaction($billdbh); }; - eval { rollback_transaction($provdbh); }; - eval { rollback_transaction($acctdbh); }; - eval { rollback_transaction($dupdbh); }; + rollback_all(); $billdbh->disconnect; $provdbh and ($provdbh->disconnect); $acctdbh->disconnect; $dupdbh and ($dupdbh->disconnect); - next; - } - if ($DBI::err == 1213) { + next; #fetch new batch + } elsif ($DBI::err == 1213) { INFO "Transaction concurrency problem, rolling back and retrying..."; - eval { rollback_transaction($billdbh); }; - eval { rollback_transaction($provdbh); }; - eval { rollback_transaction($acctdbh); }; - eval { rollback_transaction($dupdbh); }; - next; + rollback_all(); + next; #fetch new batch + } else { + rollback_all(); + FATAL $error; #terminate upon other DB errors } + } else { + rollback_all(); + FATAL $info_prefix."rating CDR ID $cdr_id aborted (failed ". + ($failed_cdr_max_retries + 1)." times), please fix it manually: " . $error; #terminate } - eval { rollback_transaction($billdbh); }; - eval { rollback_transaction($provdbh); }; - eval { rollback_transaction($acctdbh); }; - eval { rollback_transaction($dupdbh); }; - FATAL "Error rating CDR batch: " . $error; } $rated += $rated_batch; - INFO "$rated CDRs rated so far.\n"; + INFO "Batch of $rated_batch CDRs completed. $rated CDRs rated overall so far.\n"; $shutdown and last; - if ($rated >= $next_del) { + if ($rated >= $next_del) { # not ideal imho $next_del = $rated + 10000; - while ($sth_delete_old_prepaid->execute > 0) { ; } + while ($sth_delete_old_prepaid->execute > 0) { + WARNING $sth_delete_old_prepaid->rows; + } } - unless(@cdrs >= 5) - { - INFO "Less than 5 new CDRs rated, sleep $loop_interval"; + if ((scalar @cdrs) < 5) { + INFO "Less than 5 new CDRs, sleep $loop_interval"; sleep($loop_interval); - next; } + if ($failed > 0) { + INFO "There were $failed failed CDRs, sleep $failed_cdr_retry_delay"; + sleep($failed_cdr_retry_delay); + } + } INFO "Shutting down.\n"; @@ -2348,8 +2824,6 @@ sub main $sth_unrated_cdrs->finish; $sth_update_cdr->finish; $split_peak_parts and $sth_create_cdr_fragment->finish; - $sth_provider_info->finish; - $sth_reseller_info->finish; $sth_get_cbalances->finish; $sth_update_cbalance_w_underrun_profiles_lock->finish; $sth_update_cbalance_w_underrun_lock->finish; @@ -2364,7 +2838,9 @@ sub main $sth_lnp_number->finish; $sth_lnp_profile_info->finish; $sth_get_contract_info->finish; - $sth_prepaid_costs->finish; + $sth_prepaid_costs_cache->finish; + $sth_prepaid_costs_count->finish; + $sth_prepaid_cost->finish; $sth_delete_prepaid_cost->finish; $sth_delete_old_prepaid->finish; $sth_get_billing_voip_subscribers->finish; @@ -2377,7 +2853,13 @@ sub main $sth_update_usr_preference_value and $sth_update_usr_preference_value->finish; $sth_delete_usr_preference_value and $sth_delete_usr_preference_value->finish; $sth_duplicate_cdr and $sth_duplicate_cdr->finish; - + foreach (keys %cdr_col_models) { + my $model = $cdr_col_models{$_}; + $model->{write_sth}->{sth}->finish; + foreach (values %{$model->{dimension_sths}}) { + $_->{sth}->finish; + } + } $billdbh->disconnect; $provdbh->disconnect; diff --git a/t/Utils/Api.pm b/t/Utils/Api.pm index 3fcaa13..2bab96a 100644 --- a/t/Utils/Api.pm +++ b/t/Utils/Api.pm @@ -48,6 +48,7 @@ our @EXPORT_OK = qw( setup_subscriber setup_package to_pretty_json + cartesian_product ); my ($netloc) = ($uri =~ m!^https?://(.*)/?.*$!); @@ -457,11 +458,11 @@ sub _compare_interval { } } - if ($expected->{cash}) { + if (defined $expected->{cash}) { $ok = is($got->{cash_balance},$expected->{cash},$label . "check interval " . $got->{id} . " cash balance $got->{cash_balance} = $expected->{cash}") && $ok; } - if ($expected->{debit}) { + if (defined $expected->{debit}) { $ok = is($got->{cash_debit},$expected->{debit},$label . "check interval " . $got->{id} . " cash balance interval $got->{cash_debit} = $expected->{debit}") && $ok; } @@ -469,14 +470,18 @@ sub _compare_interval { $ok = is($got->{billing_profile_id},$expected->{profile},$label . "check interval " . $got->{id} . " billing profile id $got->{billing_profile_id} = $expected->{profile}") && $ok; } - if ($expected->{topups}) { + if (defined $expected->{topups}) { $ok = is($got->{topup_count},$expected->{topups},$label . "check interval " . $got->{id} . " topup count $got->{topup_count} = $expected->{topups}") && $ok; } - if ($expected->{timely_topups}) { + if (defined $expected->{timely_topups}) { $ok = is($got->{timely_topup_count},$expected->{timely_topups},$label . "check interval " . $got->{id} . " timely topup count $got->{timely_topup_count} = $expected->{timely_topups}") && $ok; } + if (defined $expected->{id}) { + $ok = is($got->{id},$expected->{id},$label . "check interval " . $got->{id} . " id = $expected->{id}") && $ok; + } + return $ok; } @@ -672,12 +677,13 @@ sub setup_subscriber { } sub setup_provider { - my ($domain_name,$rates,$networks,$provider_rate) = @_; + my ($domain_name,$rates,$networks,$provider_rate,$type) = @_; my $provider = {}; $provider->{contact} = create_systemcontact(); $provider->{contract} = create_contract( contact_id => $provider->{contact}->{id}, billing_profile_id => 1, #default profile id + type => $type // 'reseller', ); $provider->{reseller} = create_reseller( contract_id => $provider->{contract}->{id}, @@ -696,7 +702,7 @@ sub setup_provider { billing_profile_id => $provider->{profile}->{id}, ); } else { - ok(!$split_peak_parts,'provider rate required for split cdrs'); + ok(!$split_peak_parts,'split_peak_parts disabled'); #use default billing profile id, which already comes with fees. #$provider->{profile} = create_billing_profile( # reseller_id => $provider->{reseller}->{id}, @@ -719,10 +725,12 @@ sub setup_provider { push(@{$provider->{subscriber_fees}},$profile_fee); } $provider->{networks} = []; - foreach my $network_blocks (@$networks) { - push(@{$provider->{networks}},create_billing_network( - blocks => $network_blocks, - )); + if (defined $networks) { + foreach my $network_blocks (@$networks) { + push(@{$provider->{networks}},create_billing_network( + blocks => $network_blocks, + )); + } } $provider->{customers} = []; $provider->{packages} = []; @@ -734,11 +742,15 @@ sub _setup_fees { my $prepaid = delete $params{prepaid}; my $peaktime_weekdays = delete $params{peaktime_weekdays}; my $peaktime_specials = delete $params{peaktime_special}; + my $interval_free_time = delete $params{interval_free_time}; + #my $interval_free_cash = delete $params{interval_free_cash}; my $profile = create_billing_profile( reseller_id => $reseller->{id}, (defined $prepaid ? (prepaid => $prepaid) : ()), (defined $peaktime_weekdays ? (peaktime_weekdays => $peaktime_weekdays) : ()), (defined $peaktime_specials ? (peaktime_special => $peaktime_specials) : ()), + (defined $interval_free_time ? (interval_free_time => $interval_free_time) : ()), + #(defined $interval_free_cash ? (interval_free_cash => $interval_free_cash) : ()), ); my $zone = create_billing_zone( billing_profile_id => $profile->{id}, @@ -768,4 +780,62 @@ sub to_pretty_json { return JSON::to_json(shift, {pretty => 1}); # =~ s/(^\s*{\s*)|(\s*}\s*$)//rg =~ s/\n /\n/rg; } +sub cartesian_product { + + #Copyright (c) 2009 Philip R Brenan. + #This module is free software. It may be used, redistributed and/or + #modified under the same terms as Perl itself. + + my $s = shift; # Subroutine to call to process each element of the product + my @C = @_; # Lists to be multiplied + + my @c = (); # Current element of cartesian product + my @P = (); # Cartesian product + my $n = 0; # Number of elements in product + + return 0 if @C == 0; # Empty product + + @C == grep {ref eq 'ARRAY'} @C or die("Arrays of things required by cartesian"); + + # Generate each cartesian product when there are no prior cartesian products. + + my $p; $p = sub { + if (@c < @C) { + for (@{$C[@c]}) { + push @c, $_; + &$p(); + pop @c; + } + } else { + my $p = [ @c ]; + push @P, bless $p if &$s(@$p); + } + }; + + # Generate each cartesian product allowing for prior cartesian products. + + my $q; $q = sub { + if (@c < @C) { + for (@{$C[@c]}) { + push @c, $_; + &$q(); + pop @c; + } + } else { + my $p = [ map {ref $_ eq __PACKAGE__ ? @$_ : $_} @c ]; + push @P, bless $p if &$s(@$p); + } + }; + + # Determine optimal method of forming cartesian products for this call + + if (grep { grep {ref $_ eq __PACKAGE__ } @$_ } @C) { + &$q(); + } else { + &$p(); + } + + @P; +} + 1; diff --git a/t/Utils/Rateomat.pm b/t/Utils/Rateomat.pm index f90cbd8..52099b8 100644 --- a/t/Utils/Rateomat.pm +++ b/t/Utils/Rateomat.pm @@ -8,6 +8,7 @@ use Test::More; #use IPC::System::Simple qw(capturex); use Data::Dumper; use Time::HiRes qw(); +use Data::Rmap qw(); require Exporter; @@ -18,11 +19,26 @@ our @EXPORT_OK = qw( create_cdrs get_cdrs get_cdrs_by_call_id + prepare_offnet_subsriber_info prepare_cdr check_cdr check_cdrs delete_cdrs + create_prepaid_costs_cdrs + get_prepaid_costs_cdrs + prepare_prepaid_costs_cdr + check_prepaid_costs_cdr + check_prepaid_costs_cdrs + delete_prepaid_costs_cdrs get_usr_preferences + generate_call_id + decimal_to_string + check_cdr_time_balance_data + check_cdr_cash_balance_data + check_cdr_relation_data + get_cdr_time_balance_data + get_cdr_cash_balance_data + get_cdr_relation_data ); $ENV{RATEOMAT_BILLING_DB_USER} //= 'root'; @@ -46,7 +62,11 @@ my $provisioningdb_port = $ENV{RATEOMAT_PROVISIONING_DB_PORT} // '3306'; my $provisioningdb_user = $ENV{RATEOMAT_PROVISIONING_DB_USER} or die('Missing provisioning DB user setting.'); my $provisioningdb_pass = $ENV{RATEOMAT_PROVISIONING_DB_PASS}; +my $t = time; +my %offnet_domain_subscriber_map = (); + my %cdr_map = (); +my %prepaid_costs_cdr_map = (); END { my @ids = keys %cdr_map; @@ -54,6 +74,11 @@ END { my $deleted_cdrs = delete_cdrs(\@ids); diag('teardown - cdr IDs ' . join(', ',map { $_->{id}; } @$deleted_cdrs) . ' deleted'); } + @ids = keys %prepaid_costs_cdr_map; + if ((scalar @ids) > 0) { + my $deleted_prepaid_costs = delete_prepaid_costs_cdrs(\@ids); + diag('teardown - cdr IDs ' . join(', ',map { $_->{cdr}->{id}; } @$deleted_prepaid_costs) . ' and prepaid costs records deleted'); + } } sub run_rateomat { @@ -127,29 +152,6 @@ sub run_rateomat_threads { return $result; } -sub create_cdrs { - my ($cdrs) = @_; - my $is_ary = 'ARRAY' eq ref $cdrs; - my $result = ($is_ary ? [] : undef); - my $dbh; - eval { - $dbh = _connect_accounting_db(); - if ($is_ary) { - _begin_transaction($dbh); - $result = _create_cdrs($dbh,$cdrs); - _commit_transaction($dbh); - } else { - $result = _create_cdr($dbh,@_); - } - }; - if ($@) { - diag($@); - eval { _rollback_transaction($dbh); } if $is_ary; - } - _disconnect_db($dbh); - return $result; -} - sub _get_cli { my $subscriber = shift; my $pn = $subscriber->{primary_number}; @@ -164,6 +166,23 @@ sub _random_string { return join('',@chars[ map{ rand @chars } 1 .. $length ]); } +sub prepare_offnet_subsriber_info { + my ($username_primary_number,$domain) = @_; + my $n = 1 + scalar keys %offnet_domain_subscriber_map; + Data::Rmap::rmap { $_ =~ s//$n/; $_ =~ s//$n/; $_ =~ s//$t/; } ($domain); + $n = 1 + (exists $offnet_domain_subscriber_map{$domain} ? scalar keys %{$offnet_domain_subscriber_map{$domain}} : 0); + Data::Rmap::rmap { $_ =~ s//$n/; $_ =~ s//$n/; $_ =~ s//$t/; } ($username_primary_number); + my $username; + if ('HASH' eq ref $username_primary_number) { + $username = $username_primary_number->{cc} . $username_primary_number->{ac} . $username_primary_number->{sn}; + } else { + $username = $username_primary_number; + } + $offnet_domain_subscriber_map{$domain} = {} if not exists $offnet_domain_subscriber_map{$domain}; + $offnet_domain_subscriber_map{$domain}->{$username} = 1; + return { username => $username, domain => $domain }; +} + sub prepare_cdr { my ($source_subscriber, $source_peering_subsriber_info, @@ -173,12 +192,13 @@ sub prepare_cdr { $dest_reseller, $source_ip, $time, - $duration) = @_; - return { + $duration, + @overrides) = @_; + my $cdr = { #id => , #update_time => , source_user_id => ($source_subscriber ? $source_subscriber->{uuid} : '0'), - source_provider_id => $source_reseller->{contract_id}, + source_provider_id => ($source_reseller ? $source_reseller->{contract_id} : '0'), #source_external_subscriber_id => , #source_external_contract_id => , source_account_id => ($source_subscriber ? $source_subscriber->{customer_id} : '0'), @@ -198,15 +218,15 @@ sub prepare_cdr { #source_gpp8 => , #source_gpp9 => , destination_user_id => ($dest_subscriber ? $dest_subscriber->{uuid} : '0'), - destination_provider_id => $dest_reseller->{contract_id}, + destination_provider_id => ($dest_reseller ? $dest_reseller->{contract_id} : '0'), #destination_external_subscriber_id => , #destination_external_contract_id => , destination_account_id => ($dest_subscriber ? $dest_subscriber->{customer_id} : '0'), destination_user => ($dest_subscriber ? $dest_subscriber->{username} : $dest_peering_subsriber_info->{username}), - destination_domain => ($dest_subscriber ? $dest_subscriber->{domain} : $source_peering_subsriber_info->{domain}), - destination_user_dialed => ($dest_subscriber ? $dest_subscriber->{username} : $source_peering_subsriber_info->{username}), - destination_user_in => ($dest_subscriber ? $dest_subscriber->{username} : $source_peering_subsriber_info->{username}), - destination_domain_in => ($dest_subscriber ? $dest_subscriber->{domain} : $source_peering_subsriber_info->{domain}), + destination_domain => ($dest_subscriber ? $dest_subscriber->{domain} : $dest_peering_subsriber_info->{domain}), + destination_user_dialed => ($dest_subscriber ? $dest_subscriber->{username} : $dest_peering_subsriber_info->{username}), + destination_user_in => ($dest_subscriber ? $dest_subscriber->{username} : $dest_peering_subsriber_info->{username}), + destination_domain_in => ($dest_subscriber ? $dest_subscriber->{domain} : $dest_peering_subsriber_info->{domain}), #destination_gpp0 => , #destination_gpp1 => , #destination_gpp2 => , @@ -225,7 +245,7 @@ sub prepare_cdr { init_time => $time, start_time => $time, duration => $duration, - call_id => '*TEST*'._random_string(26,'a'..'z','A'..'Z',0..9,'-','.'), + call_id => generate_call_id(), #source_carrier_cost => , #source_reseller_cost => , #source_customer_cost => , @@ -259,7 +279,58 @@ sub prepare_cdr { #rating_status => 'unrated', #exported_at => , #export_status => , + @overrides }; + return $cdr; +} + +sub prepare_prepaid_costs_cdr { + + my ($source_subscriber, + $source_peering_subsriber_info, + $source_reseller, + $dest_subscriber, + $dest_peering_subsriber_info, + $dest_reseller, + $source_ip, + $time, + $duration, + $prepaid_cost, + $prepaid_free_time_used, + @overrides) = @_; + my $cdr = prepare_cdr($source_subscriber, + $source_peering_subsriber_info, + $source_reseller, + $dest_subscriber, + $dest_peering_subsriber_info, + $dest_reseller, + $source_ip, + $time, + $duration, + @overrides); + my ($S, $M, $H, $d, $m, $Y) = localtime($time); + return { cdr => $cdr, + prepaid_costs => { + #id => + call_id => $cdr->{call_id}, + cost => $prepaid_cost, + free_time_used => $prepaid_free_time_used // 0, + timestamp => sprintf("%04d-%02d-%02d %02d:%02d:%02d", $Y + 1900,$m + 1, $d, $H, $M, $S), + }, + }; +} + +sub generate_call_id { + return '*TEST*'._random_string(26,'a'..'z','A'..'Z',0..9,'-','.'); +} + +sub decimal_to_string { + my $value = shift; + if (defined $value) { + return sprintf('%6f',$value); + } else { + return undef; + } } sub check_cdrs { @@ -285,6 +356,76 @@ sub check_cdr { } +sub check_prepaid_costs_cdrs { + my ($label,$prepaid_costs_exists,%expected_map) = @_; + my $ok = 1; + foreach my $id (keys %expected_map) { + $ok = check_prepaid_costs_cdr($label,$id,$expected_map{$id},$prepaid_costs_exists) && $ok; + } + return $ok; +} + +sub check_prepaid_costs_cdr { + + my ($label,$id,$expected_cdr,$prepaid_costs_exists) = @_; + + my $got_prepaid_costs_cdr = get_prepaid_costs_cdrs($id); + my $ok = 1; + foreach my $field (keys %$expected_cdr) { + $ok = is($got_prepaid_costs_cdr->{cdr}->{$field},$expected_cdr->{$field},$label . $field . ' = ' . $expected_cdr->{$field}) && $ok; + } + $ok = is(defined $got_prepaid_costs_cdr->{prepaid_costs} ? '1' : '0',$prepaid_costs_exists,$label . 'prepaid cost record ' . ($prepaid_costs_exists ? 'exists' : 'does not exist')) && $ok; + diag(Dumper({result_cdr => $got_prepaid_costs_cdr})) if !$ok; + return $ok; + +} + +sub create_cdrs { + my ($cdrs) = @_; + my $is_ary = 'ARRAY' eq ref $cdrs; + my $result = ($is_ary ? [] : undef); + my $dbh; + eval { + $dbh = _connect_accounting_db(); + if ($is_ary) { + _begin_transaction($dbh); + $result = _create_cdrs($dbh,$cdrs); + _commit_transaction($dbh); + } else { + $result = _create_cdr($dbh,@_); + } + }; + if ($@) { + diag($@); + eval { _rollback_transaction($dbh); } if $is_ary; + } + _disconnect_db($dbh); + return $result; +} + +sub create_prepaid_costs_cdrs { + my ($cdrs) = @_; + my $is_ary = 'ARRAY' eq ref $cdrs; + my $result = ($is_ary ? [] : undef); + my $dbh; + eval { + $dbh = _connect_accounting_db(); + if ($is_ary) { + _begin_transaction($dbh); + $result = _create_prepaid_costs_cdrs($dbh,$cdrs); + _commit_transaction($dbh); + } else { + $result = _create_prepaid_costs_cdr($dbh,@_); + } + }; + if ($@) { + diag($@); + eval { _rollback_transaction($dbh); } if $is_ary; + } + _disconnect_db($dbh); + return $result; +} + sub _create_cdr { my ($dbh,%values) = @_; if ($dbh) { @@ -294,6 +435,19 @@ sub _create_cdr { return undef; } +sub _create_prepaid_costs_cdr { + my ($dbh,$values) = @_; + if ($dbh) { + if (_insert($dbh,'accounting.prepaid_costs',$values->{prepaid_costs})) { + my $id = _insert($dbh,'accounting.cdr',$values->{cdr}); + return _get_prepaid_costs_cdr($dbh,$id,1) if ok($id,'cdr id '.$id.' with prepaid costs record created'); + } else { + fail('creating prepaid costs for call_id '.$values->{prepaid_costs}->{call_id}.' record'); + } + } + return { prepaid_costs => undef, cdr => undef }; +} + sub _create_cdrs { my ($dbh,$cdrs) = @_; if ($dbh) { @@ -307,6 +461,23 @@ sub _create_cdrs { return []; } +sub _create_prepaid_costs_cdrs { + my ($dbh,$prepaid_costs_cdrs) = @_; + if ($dbh) { + my @ids = (); + foreach my $values (@$prepaid_costs_cdrs) { + if (_insert($dbh,'accounting.prepaid_costs',$values->{prepaid_costs})) { + my $id = _insert($dbh,'accounting.cdr',$values->{cdr}); + push(@ids,$id) if $id; + } else { + fail('creating prepaid costs for call_id '.$values->{prepaid_costs}->{call_id}.' record'); + } + } + return _get_prepaid_costs_cdrs($dbh,\@ids,1) if ok((scalar @ids) == (scalar @$prepaid_costs_cdrs),'cdrs id '.join(', ',@ids).' with prepaid costs records created'); + }; + return []; +} + sub get_cdrs { my $ids = shift; my $is_ary = 'ARRAY' eq ref $ids; @@ -323,6 +494,22 @@ sub get_cdrs { return $result; } +sub get_prepaid_costs_cdrs { + my $ids = shift; + my $is_ary = 'ARRAY' eq ref $ids; + my $result = ($is_ary ? [] : undef); + eval { + my $dbh = _connect_accounting_db(); + $result = _get_prepaid_costs_cdrs($dbh,$ids) if $is_ary; + $result = _get_prepaid_costs_cdr($dbh,$ids) if !$is_ary; + _disconnect_db($dbh); + }; + if ($@) { + diag($@); + } + return $result; +} + sub get_cdrs_by_call_id { my $call_id = shift; my $result = []; @@ -345,8 +532,28 @@ sub delete_cdrs { eval { $dbh = _connect_accounting_db(); _begin_transaction($dbh); - $result = _delete($dbh,'accounting.cdr',$ids) if $is_ary; - $result = _delete($dbh,'accounting.cdr',[ $ids ])->[0] if !$is_ary; + $result = _delete_cdrs($dbh,$ids) if $is_ary; + $result = _delete_cdrs($dbh,[ $ids ])->[0] if !$is_ary; + _commit_transaction($dbh); + }; + if ($@) { + diag($@); + eval { _rollback_transaction($dbh); }; + } + _disconnect_db($dbh); + return $result; +} + +sub delete_prepaid_costs_cdrs { + my $ids = shift; + my $is_ary = 'ARRAY' eq ref $ids; + my $result = ($is_ary ? [] : undef); + my $dbh; + eval { + $dbh = _connect_accounting_db(); + _begin_transaction($dbh); + $result = _delete_prepaid_costs_cdrs($dbh,$ids) if $is_ary; + $result = _delete_prepaid_costs_cdrs($dbh,[ $ids ])->[0] if !$is_ary; _commit_transaction($dbh); }; if ($@) { @@ -371,45 +578,26 @@ sub _get_cdr { return $cdr; } -sub get_usr_preferences { - my ($subscriber,$attribute) = @_; - my $usr_prefs = []; - my $dbh; - eval { - $dbh = _connect_provisioning_db(); - _begin_transaction($dbh); - my $sth = $dbh->prepare('SELECT * FROM provisioning.voip_preferences WHERE attribute = ?') - or die("Error preparing select voip preferences statement: ".$dbh->errstr); - $sth->execute($attribute); - my $pref = $sth->fetchrow_hashref(); +sub _get_prepaid_costs_cdr { + my ($dbh,$id,$created) = @_; + my $prepaid_costs_cdr = undef; + if ($dbh) { + my $sth = $dbh->prepare('SELECT * FROM accounting.cdr WHERE id = ?') + or die("Error preparing select cdr statement: ".$dbh->errstr); + $sth->execute($id); + my $cdr = $sth->fetchrow_hashref(); $sth->finish(); - if (defined $pref && $pref->{id}) { - $sth = $dbh->prepare('SELECT id FROM provisioning.voip_subscribers WHERE uuid = ? LIMIT 1') - or die("Error preparing select voip subscribers statement: ".$dbh->errstr); - $sth->execute($subscriber->{uuid}); - my ($prov_subscriber_id) = $sth->fetchrow_array(); + if ($cdr) { + $sth = $dbh->prepare('SELECT * FROM accounting.prepaid_costs WHERE call_id = ?') + or die("Error preparing select prepaid_costs statement: ".$dbh->errstr); + $sth->execute($cdr->{call_id}); + my $prepaid_costs = $sth->fetchrow_hashref(); + $prepaid_costs_cdr = { cdr => $cdr, prepaid_costs => $prepaid_costs }; + $prepaid_costs_cdr_map{$cdr->{id}} = $prepaid_costs_cdr if $created; $sth->finish(); - if ($prov_subscriber_id) { - $sth = $dbh->prepare('SELECT * FROM provisioning.voip_usr_preferences WHERE subscriber_id = ? AND attribute_id = ?') - or die("Error preparing select voip usr preferences statement: ".$dbh->errstr); - $sth->execute($prov_subscriber_id,$pref->{id}); - $usr_prefs = $sth->fetchall_arrayref({}); - $sth->finish(); - } else { - die("cannot find prov subscriber '$subscriber->{uuid}'"); - } - } else { - die("cannot find attribute '$attribute'"); } - _commit_transaction($dbh); - _disconnect_db($dbh); - }; - if ($@) { - diag($@); - eval { _rollback_transaction($dbh); }; } - _disconnect_db($dbh); - return $usr_prefs; + return $prepaid_costs_cdr; } sub _get_cdrs { @@ -430,6 +618,31 @@ sub _get_cdrs { return $cdrs; } +sub _get_prepaid_costs_cdrs { + my ($dbh,$ids,$created) = @_; + my @result = (); + if ($dbh) { + my $sth = $dbh->prepare('SELECT * FROM accounting.cdr WHERE id IN ('.substr(',?' x scalar @$ids,1).') ORDER BY id') + or die("Error preparing select cdrs statement: ".$dbh->errstr); + $sth->execute(@$ids); + my $cdrs = $sth->fetchall_arrayref({}); + $sth->finish(); + my $prepaid_costs; + my $prepaid_costs_cdr; + foreach my $cdr (@$cdrs) { + $sth = $dbh->prepare('SELECT * FROM accounting.prepaid_costs WHERE call_id = ?') + or die("Error preparing select prepaid_costs statement: ".$dbh->errstr); + $sth->execute($cdr->{call_id}); + $prepaid_costs = $sth->fetchrow_hashref(); + $sth->finish(); + $prepaid_costs_cdr = { cdr => $cdr, prepaid_costs => $prepaid_costs }; + $prepaid_costs_cdr_map{$cdr->{id}} = $prepaid_costs_cdr if $created; + push(@result,$prepaid_costs_cdr); + } + } + return \@result; +} + sub _get_cdrs_by_callid { my ($dbh,$call_id,$created) = @_; my $cdrs = []; @@ -464,13 +677,14 @@ sub _insert { } return $id; } -sub _delete { - my ($dbh,$table,$ids) = @_; +sub _delete_cdrs { + my ($dbh,$ids) = @_; my $deleted = []; if ($dbh) { $deleted = _get_cdrs($dbh,$ids); - my $sth = $dbh->prepare('DELETE FROM ' . $table . ' WHERE id IN ('.substr(',?' x scalar @$ids,1).')') - or die("Error preparing delete statement: ".$dbh->errstr); + _delete_all_cdr_data($dbh,$ids); + my $sth = $dbh->prepare('DELETE FROM accounting.cdr WHERE id IN ('.substr(',?' x scalar @$ids,1).')') + or die("Error preparing delete cdrs statement: ".$dbh->errstr); $sth->execute(@$ids); $sth->finish(); foreach my $id (@$ids) { @@ -480,6 +694,278 @@ sub _delete { return $deleted; } +sub _delete_prepaid_costs_cdrs { + my ($dbh,$ids) = @_; + my $deleted = []; + if ($dbh) { + $deleted = _get_prepaid_costs_cdrs($dbh,$ids); + _delete_all_cdr_data($dbh,$ids); + my $sth = $dbh->prepare('DELETE cdr,prepaid_costs FROM '. + 'accounting.cdr AS cdr '. + 'LEFT JOIN accounting.prepaid_costs AS prepaid_costs ON cdr.call_id = prepaid_costs.call_id '. + 'WHERE cdr.id IN ('.substr(',?' x scalar @$ids,1).')') + or die("Error preparing delete cdrs with prepaid costs statement: ".$dbh->errstr); + $sth->execute(@$ids); + $sth->finish(); + foreach my $id (@$ids) { + delete $prepaid_costs_cdr_map{$id}; + } + } + return $deleted; +} + +sub get_cdr_relation_data { + my $label = shift; + my $cdr_id = shift; + my $direction = shift; + my $provider = shift; + my $relation = shift; + $label .= 'cdr id '.$cdr_id.' '.$direction.'_'.$provider.'_'.$relation.' '; + my $result = _get_cdr_relation_data($cdr_id,$direction,$provider,$relation); + is(scalar @$result,1,$label.'number of records '.(scalar @$result).' = 1'); + return $result->[0]->{val}; +} + + +sub get_cdr_cash_balance_data { + my $label = shift; + my $cdr_id = shift; + my $direction = shift; + my $provider = shift; + my $cash_balance = shift; + $label .= 'cdr id '.$cdr_id.' '.$direction.'_'.$provider.'_'.$cash_balance.' '; + my $result = _get_cdr_cash_balance_data($cdr_id,$direction,$provider,$cash_balance); + is(scalar @$result,1,$label.'number of records '.(scalar @$result).' = 1'); + return { before => $result->[0]->{val_before}, after => $result->[0]->{val_after} }; +} + +sub get_cdr_time_balance_data { + my $label = shift; + my $cdr_id = shift; + my $direction = shift; + my $provider = shift; + my $time_balance = shift; + $label .= 'cdr id '.$cdr_id.' '.$direction.'_'.$provider.'_'.$time_balance.' '; + my $result = _get_cdr_time_balance_data($cdr_id,$direction,$provider,$time_balance); + is(scalar @$result,1,$label.'number of records '.(scalar @$result).' = 1'); + return { before => $result->[0]->{val_before}, after => $result->[0]->{val_after} }; +} + +sub check_cdr_relation_data { + my $label = shift; + my $cdr_id = shift; + my $direction = shift; + my $provider = shift; + my $relation = shift; + my $expected = shift; + $label .= 'cdr id '.$cdr_id.' '.$direction.'_'.$provider.'_'.$relation.' '; + my $result = _get_cdr_relation_data($cdr_id,$direction,$provider,$relation); + if (defined $expected) { + if ((scalar @$result) == 1) { + return is($result->[0]->{val},$expected,$label.$result->[0]->{val}.' = '.$expected); + } else { + return is(scalar @$result,1,$label.'number of records '.(scalar @$result).' = 1'); + } + } else { + return is(scalar @$result,0,$label.'number of records '.(scalar @$result).' = 0'); + } + +} + +sub check_cdr_cash_balance_data { + my $label = shift; + my $cdr_id = shift; + my $direction = shift; + my $provider = shift; + my $relation = shift; + my $expected = shift; + $label .= 'cdr id '.$cdr_id.' '.$direction.'_'.$provider.'_'.$relation.' '; + my $result = _get_cdr_cash_balance_data($cdr_id,$direction,$provider,$relation); + if (defined $expected) { + if ((scalar @$result) == 1) { + return is($result->[0]->{val_before},decimal_to_string($expected->{before}),$label.'before '.$result->[0]->{val_before}.' = '.decimal_to_string($expected->{before})) & + is($result->[0]->{val_after},decimal_to_string($expected->{after}),$label.'after '.$result->[0]->{val_after}.' = '.decimal_to_string($expected->{after})); + } else { + return is(scalar @$result,1,$label.'number of records '.(scalar @$result).' = 1'); + } + } else { + return is(scalar @$result,0,$label.'number of records '.(scalar @$result).' = 0'); + } + +} + +sub check_cdr_time_balance_data { + + my $label = shift; + my $cdr_id = shift; + my $direction = shift; + my $provider = shift; + my $relation = shift; + my $expected = shift; + $label .= 'cdr id '.$cdr_id.' '.$direction.'_'.$provider.'_'.$relation.' '; + my $result = _get_cdr_time_balance_data($cdr_id,$direction,$provider,$relation); + if (defined $expected) { + if ((scalar @$result) == 1) { + return is($result->[0]->{val_before},$expected->{before},$label.'before '.$result->[0]->{val_before}.' = '.$expected->{before}) & + is($result->[0]->{val_after},$expected->{after},$label.'after '.$result->[0]->{val_after}.' = '.$expected->{after}); + } else { + return is(scalar @$result,1,$label.'number of records '.(scalar @$result).' = 1'); + } + } else { + return is(scalar @$result,0,$label.'number of records '.(scalar @$result).' = 0'); + } + +} + +sub _get_cdr_relation_data { + my $id = shift; + my $direction = shift; + my $provider = shift; + my $relation = shift; + my $result = undef; + eval { + my $dbh = _connect_accounting_db(); + $result = _get_cdr_data($dbh,$id,'accounting.cdr_relation_data',[ + { type => $direction, table => 'accounting.cdr_direction', data_col => 'direction_id' }, + { type => $provider, table => 'accounting.cdr_provider', data_col => 'provider_id' }, + { type => $relation, table => 'accounting.cdr_relation', data_col => 'relation_id' }, + ]); + _disconnect_db($dbh); + }; + if ($@) { + diag($@); + } + return $result; +} + +sub _get_cdr_cash_balance_data { + my $id = shift; + my $direction = shift; + my $provider = shift; + my $cash_balance = shift; + my $result = undef; + eval { + my $dbh = _connect_accounting_db(); + $result = _get_cdr_data($dbh,$id,'accounting.cdr_cash_balance_data',[ + { type => $direction, table => 'accounting.cdr_direction', data_col => 'direction_id' }, + { type => $provider, table => 'accounting.cdr_provider', data_col => 'provider_id' }, + { type => $cash_balance, table => 'accounting.cdr_cash_balance', data_col => 'cash_balance_id' }, + ]); + _disconnect_db($dbh); + }; + if ($@) { + diag($@); + } + return $result; +} + +sub _get_cdr_time_balance_data { + my $id = shift; + my $direction = shift; + my $provider = shift; + my $time_balance = shift; + my $result = undef; + eval { + my $dbh = _connect_accounting_db(); + $result = _get_cdr_data($dbh,$id,'accounting.cdr_time_balance_data',[ + { type => $direction, table => 'accounting.cdr_direction', data_col => 'direction_id' }, + { type => $provider, table => 'accounting.cdr_provider', data_col => 'provider_id' }, + { type => $time_balance, table => 'accounting.cdr_time_balance', data_col => 'time_balance_id' }, + ]); + _disconnect_db($dbh); + }; + if ($@) { + diag($@); + } + return $result; +} + +sub _get_cdr_data { + my ($dbh,$id,$table,$dimensions) = @_; + my $cdr_data = undef; + if ($dbh) { + my $sth; + my @dimension_ids = (); + my $dimension_clause = ''; + for my $dimension (@$dimensions) { + $sth = $dbh->prepare('SELECT id FROM '.$dimension->{table}.' WHERE type = ?') + or die("Error preparing select cdr data dimension statement: ".$dbh->errstr); + $sth->execute($dimension->{type}); + my ($dimension_id) = $sth->fetchrow_array(); + $sth->finish(); + push(@dimension_ids,$dimension_id); + $dimension_clause .= ' AND ' . $dimension->{data_col} . ' = ?'; + } + $sth = $dbh->prepare('SELECT * FROM '.$table.' WHERE cdr_id = ?' . $dimension_clause) + or die("Error preparing select cdr data statement: ".$dbh->errstr); + $sth->execute($id,@dimension_ids); + $cdr_data = $sth->fetchall_arrayref({}); + $sth->finish(); + } + return $cdr_data; +} + +sub _delete_all_cdr_data { + my ($dbh,$ids) = @_; + _delete_cdr_data($dbh,$ids,'accounting.cdr_relation_data'); + _delete_cdr_data($dbh,$ids,'accounting.cdr_cash_balance_data'); + _delete_cdr_data($dbh,$ids,'accounting.cdr_time_balance_data'); +} + +sub _delete_cdr_data { # as long as no triggers are present + my ($dbh,$ids,$table) = @_; + if ($dbh) { + my $sth = $dbh->prepare('DELETE FROM '.$table.' WHERE cdr_id IN ('.substr(',?' x scalar @$ids,1).')') + or die("Error preparing delete cdr data statement: ".$dbh->errstr); + $sth->execute(@$ids); + if ($sth->rows > 0) { + diag($sth->rows . " cdr $table rows deleted"); + } + $sth->finish(); + } +} + +sub get_usr_preferences { + my ($subscriber,$attribute) = @_; + my $usr_prefs = []; + my $dbh; + eval { + $dbh = _connect_provisioning_db(); + _begin_transaction($dbh); + my $sth = $dbh->prepare('SELECT * FROM provisioning.voip_preferences WHERE attribute = ?') + or die("Error preparing select voip preferences statement: ".$dbh->errstr); + $sth->execute($attribute); + my $pref = $sth->fetchrow_hashref(); + $sth->finish(); + if (defined $pref && $pref->{id}) { + $sth = $dbh->prepare('SELECT id FROM provisioning.voip_subscribers WHERE uuid = ? LIMIT 1') + or die("Error preparing select voip subscribers statement: ".$dbh->errstr); + $sth->execute($subscriber->{uuid}); + my ($prov_subscriber_id) = $sth->fetchrow_array(); + $sth->finish(); + if ($prov_subscriber_id) { + $sth = $dbh->prepare('SELECT * FROM provisioning.voip_usr_preferences WHERE subscriber_id = ? AND attribute_id = ?') + or die("Error preparing select voip usr preferences statement: ".$dbh->errstr); + $sth->execute($prov_subscriber_id,$pref->{id}); + $usr_prefs = $sth->fetchall_arrayref({}); + $sth->finish(); + } else { + die("cannot find prov subscriber '$subscriber->{uuid}'"); + } + } else { + die("cannot find attribute '$attribute'"); + } + _commit_transaction($dbh); + _disconnect_db($dbh); + }; + if ($@) { + diag($@); + eval { _rollback_transaction($dbh); }; + } + _disconnect_db($dbh); + return $usr_prefs; +} + sub _connect_accounting_db { my $dbh = DBI->connect("dbi:mysql:database=$accountingdb_name;host=$accountingdb_host;port=$accountingdb_port", $accountingdb_user, $accountingdb_pass, diff --git a/t/rateomat-01-run.t b/t/rateomat-01-run.t new file mode 100644 index 0000000..5fc41cf --- /dev/null +++ b/t/rateomat-01-run.t @@ -0,0 +1,22 @@ + +use strict; + +use Utils::Api qw(); +use Utils::Rateomat qw(); +use Test::More; + +### testcase outline: +### rate-o-mat.pl invocation test +### +### this test verifies that ratomat can be properly invoked +### by the .t test scripts here. any further testcases can +### be skipped unless this one succeeds. + +{ + + ok(Utils::Rateomat::run_rateomat_threads(1,5),"rate-o-mat executed"); + +} + +done_testing(); +exit; diff --git a/t/rateomat-05-parallel.t b/t/rateomat-05-parallel.t new file mode 100644 index 0000000..e009b9b --- /dev/null +++ b/t/rateomat-05-parallel.t @@ -0,0 +1,121 @@ + +use strict; + +use Utils::Api qw(); +use Utils::Rateomat qw(); +use Test::More; + +### testcase outline: +### onnet calls between subscribers of multiple resellers +### are rated by multiple rateomat instances running +### concurrently +### +### this tests verify that ratomat can be run safely against +### one and the same accounting.cdr table. + +{ + + my $number_of_rateomat_threads = 3; + my $rateomat_timeout = 60; + my $number_of_providers = 3; + my $number_of_subscribers_per_provider = 3; + my $balance = 0.0; + my %subscribers = (); + foreach (1..$number_of_providers) { + my $rate_interval = 30 + int(rand(31)); + my $provider = create_provider($rate_interval); + + my $caller_fee = $provider->{subscriber_fees}->[0]; + + foreach (1..$number_of_subscribers_per_provider) { + my $subscriber = Utils::Api::setup_subscriber($provider,$caller_fee->{profile},$balance,{ cc => 888, ac => '3', sn => '' }); + $subscriber->{provider} = $provider; + $subscriber->{fee} = $caller_fee; + $subscriber->{rate_interval} = $rate_interval; + $subscribers{$subscriber->{customer}->{id}} = $subscriber; + } + } + + my @caller_callee_matrix = (); + # add calls from each subscriber to each subscriber except itself: + Utils::Api::cartesian_product(sub { + my ($caller,$callee) = @_; + push(@caller_callee_matrix,{ caller => $caller, callee => $callee }) unless $caller->{customer}->{id} == $callee->{customer}->{id}; + },[ values %subscribers ],[ values %subscribers ]); + ## add calls from each subscriber to itself: + #Utils::Api::cartesian_product(sub { + # my ($caller,$callee) = @_; + # push(@caller_callee_matrix,{ caller => $caller, callee => $callee }) if $caller->{customer}->{id} == $callee->{customer}->{id}; + #},[ values %subscribers ],[ values %subscribers ]); + + # place calls by generating cdrs: + my @cdr_ids = map { $_->{id}; } @{ Utils::Rateomat::create_cdrs([ map { + Utils::Rateomat::prepare_cdr($_->{caller}->{subscriber},undef,$_->{caller}->{reseller}, + $_->{callee}->{subscriber},undef,$_->{callee}->{reseller}, + '192.168.0.1',Utils::Api::current_unix(),$_->{caller}->{rate_interval} + 1); + } @caller_callee_matrix ]) }; + my $number_of_calls = ($number_of_providers * $number_of_subscribers_per_provider) * ($number_of_providers * $number_of_subscribers_per_provider - 1); + $ENV{RATEOMAT_BATCH_SIZE} = $number_of_calls; # ensure all rateomats grab all cdrs at once + + $ENV{RATEOMAT_SHUFFLE_BATCH} = 1; # enable and see the speedup + + if (ok((scalar @cdr_ids) == $number_of_calls,'there are '.$number_of_calls.' calls to rate') + && ok(Utils::Rateomat::run_rateomat_threads($number_of_rateomat_threads, $rateomat_timeout),'rate-o-mat threads executed')) { + + ok(Utils::Rateomat::check_cdrs('', + map { + my $cdr = Utils::Rateomat::get_cdrs($_); + my $caller = $subscribers{$cdr->{source_account_id}}; + my $call_costs = $caller->{fee}->{fees}->[0]->{onpeak_init_rate} * + $caller->{fee}->{fees}->[0]->{onpeak_init_interval} + + $caller->{fee}->{fees}->[0]->{onpeak_follow_rate} * + $caller->{fee}->{fees}->[0]->{onpeak_follow_interval}; + $caller->{call_costs} += $call_costs; + $_ => { + id => $_, + rating_status => 'ok', + source_customer_cost => Utils::Rateomat::decimal_to_string($call_costs), + destination_customer_cost => Utils::Rateomat::decimal_to_string(0.0), + source_reseller_cost => Utils::Rateomat::decimal_to_string(0.0), + destination_reseller_cost => Utils::Rateomat::decimal_to_string(0.0), + }; + } @cdr_ids + ),'cdrs were all processed'); + + foreach (keys %subscribers) { + my $caller = $subscribers{$_}; + Utils::Api::check_interval_history("caller $_: ",$_,[ + { #cash => (100.0 * $balance - $caller->{call_costs})/100.0, + debit => $caller->{call_costs}/100.0, + }, + ]); + } + + } + +} + + +done_testing(); +exit; + +sub create_provider { + my $rate_interval = shift; + $rate_interval //= 60; + return Utils::Api::setup_provider('test.com', + [ #rates: + { #any + onpeak_init_rate => 2, + onpeak_init_interval => $rate_interval, + onpeak_follow_rate => 1, + onpeak_follow_interval => $rate_interval, + offpeak_init_rate => 2, + offpeak_init_interval => $rate_interval, + offpeak_follow_rate => 1, + offpeak_follow_interval => $rate_interval, + }, + ], + [ #billing networks: + ] + ); +} diff --git a/t/rateomat-10-prepaid-costs.t b/t/rateomat-10-prepaid-costs.t new file mode 100644 index 0000000..9ff7ab3 --- /dev/null +++ b/t/rateomat-10-prepaid-costs.t @@ -0,0 +1,77 @@ +use strict; + +use Utils::Api qw(); +use Utils::Rateomat qw(); +use Test::More; + +### testcase outline: +### onnet prepaid calls with costs from prepaid costs +### table +### +### this tests verify that prepaid costs are properly +### cached and cleaned up. + +my $provider = Utils::Api::setup_provider('test.com', + [ #rates: + { + prepaid => 1, + onpeak_init_rate => 2, + onpeak_init_interval => 60, + onpeak_follow_rate => 1, + onpeak_follow_interval => 30, + offpeak_init_rate => 2, + offpeak_init_interval => 60, + offpeak_follow_rate => 1, + offpeak_follow_interval => 30, + }, + ], +); + +my $call_costs = ($provider->{subscriber_fees}->[0]->{fee}->{onpeak_init_rate} * + $provider->{subscriber_fees}->[0]->{fee}->{onpeak_init_interval} + + $provider->{subscriber_fees}->[0]->{fee}->{onpeak_follow_rate} * + $provider->{subscriber_fees}->[0]->{fee}->{onpeak_follow_interval})/100.0; + +my $call_count = 3; +my $balance = $call_count * $call_costs; +my $profiles_setup = $provider->{subscriber_fees}->[0]->{profile}; +my $caller = Utils::Api::setup_subscriber($provider,$profiles_setup,$balance,{ cc => 888, ac => '1', sn => '' }); +#my $caller2 = Utils::Api::setup_subscriber($provider,$profiles_setup,$balance,{ cc => 888, ac => '1', sn => '' }); +#my $caller3 = Utils::Api::setup_subscriber($provider,$profiles_setup,$balance,{ cc => 888, ac => '1', sn => '' }); +my $callee = Utils::Api::setup_subscriber($provider,$profiles_setup,$balance,{ cc => 888, ac => '2', sn => '' }); + +foreach my $cache_size (($call_count - 1,$call_count)) { + diag('rateomat prepaid costs cache size: '.$cache_size); + $ENV{RATEOMAT_PREPAID_COSTS_CACHE} = $cache_size; + + my @cdr_ids = map { $_->{cdr}->{id}; } @{ Utils::Rateomat::create_prepaid_costs_cdrs([ map { + Utils::Rateomat::prepare_prepaid_costs_cdr($caller->{subscriber},undef,$caller->{reseller}, + $callee->{subscriber},undef,$callee->{reseller}, + '192.168.0.1',Utils::Api::current_unix(), + $provider->{subscriber_fees}->[0]->{fee}->{onpeak_init_interval} + 1, + $call_costs,0); + } (1..$call_count)]) }; + + ok(Utils::Rateomat::check_prepaid_costs_cdrs('',1, + map { $_ => { + id => $_, + rating_status => 'unrated', + }; } @cdr_ids + ),'cdrs and prepaid costs were all prepared'); + + if (ok((scalar @cdr_ids) > 0 && Utils::Rateomat::run_rateomat_threads(1),'rate-o-mat executed')) { + ok(Utils::Rateomat::check_prepaid_costs_cdrs('',0, + map { $_ => { + id => $_, + rating_status => 'ok', + source_customer_cost => Utils::Rateomat::decimal_to_string($call_costs), + }; } @cdr_ids + ),'cdrs were all processed'); + Utils::Api::check_interval_history('',$caller->{customer}->{id},[ + { cash => $balance }, # rateomat must not touch balance + ]); + } +} + +done_testing(); +exit; diff --git a/t/rateomat-20-basic-onnet.t b/t/rateomat-20-basic-onnet.t new file mode 100644 index 0000000..66f1fd8 --- /dev/null +++ b/t/rateomat-20-basic-onnet.t @@ -0,0 +1,296 @@ +use strict; + +use Utils::Api qw(); +use Utils::Rateomat qw(); +use Test::More; + +### testcase outline: +### onnet prepaid/postpaid calls of callers to callees with both using +### dedicated reseller fees. +### +### this tests verify all combinations of prepaid/postpaid subscriber customers with +### balance > 0.0/no balance produce correct customer/reseller call cost, cash balance +### and cash balance interval values. + +my $init_secs = 60; +my $follow_secs = 30; +my $provider_a = create_provider('testa.com'); +my $provider_b = create_provider('testb.com'); + +my $total_caller_reseller_call_costs = 0.0; +my $total_callee_reseller_call_costs = 0.0; + +# full matrix: +foreach my $prepaid ((0,1)) { # prepaid, postpaid + foreach my $balance ((0.0,10.0)) { # zero balance, enough balance + foreach my $prepaid_costs (0..1) { # prepaid: with prepaid_costs records and without (swrate down) + next if (!$prepaid && $prepaid_costs); + my $caller_fee = ($prepaid ? $provider_a->{subscriber_fees}->[1] : $provider_a->{subscriber_fees}->[0]); + my $caller1 = Utils::Api::setup_subscriber($provider_a,$caller_fee->{profile},$balance,{ cc => 888, ac => '1', sn => '' }); + my $caller2 = Utils::Api::setup_subscriber($provider_a,$caller_fee->{profile},$balance,{ cc => 888, ac => '1', sn => '' }); + my $caller3 = Utils::Api::setup_subscriber($provider_a,$caller_fee->{profile},$balance,{ cc => 888, ac => '1', sn => '' }); + my $callee_fee = ($prepaid ? $provider_b->{subscriber_fees}->[1] : $provider_b->{subscriber_fees}->[0]); + my $callee = Utils::Api::setup_subscriber($provider_b,$callee_fee->{profile},$balance,{ cc => 888, ac => '2', sn => '' }); + + my $caller_call_costs = $caller_fee->{fees}->[0]->{onpeak_init_rate} * + $caller_fee->{fees}->[0]->{onpeak_init_interval} + + $caller_fee->{fees}->[0]->{onpeak_follow_rate} * + $caller_fee->{fees}->[0]->{onpeak_follow_interval}; + my $caller_reseller_call_costs = $provider_a->{provider_fee}->{fees}->[0]->{onpeak_init_rate} * + $provider_a->{provider_fee}->{fees}->[0]->{onpeak_init_interval} + + $provider_a->{provider_fee}->{fees}->[0]->{onpeak_follow_rate} * + $provider_a->{provider_fee}->{fees}->[0]->{onpeak_follow_interval}; + + my $callee_call_costs = $callee_fee->{fees}->[1]->{onpeak_init_rate} * + $callee_fee->{fees}->[1]->{onpeak_init_interval} + + $callee_fee->{fees}->[1]->{onpeak_follow_rate} * + $callee_fee->{fees}->[1]->{onpeak_follow_interval}; + my $callee_reseller_call_costs = $provider_b->{provider_fee}->{fees}->[1]->{onpeak_init_rate} * + $provider_b->{provider_fee}->{fees}->[1]->{onpeak_init_interval} + + $provider_b->{provider_fee}->{fees}->[1]->{onpeak_follow_rate} * + $provider_b->{provider_fee}->{fees}->[1]->{onpeak_follow_interval}; + + my @cdr_ids; + my $start_time = Utils::Api::current_unix() - 5; + if ($prepaid_costs) { + @cdr_ids = map { $_->{cdr}->{id}; } @{ Utils::Rateomat::create_prepaid_costs_cdrs([ map { + Utils::Rateomat::prepare_prepaid_costs_cdr($_->{subscriber},undef,$_->{reseller}, + $callee->{subscriber},undef,$callee->{reseller}, + '192.168.0.1',$start_time += 1,$init_secs + $follow_secs, + $caller_call_costs,0); + } ($caller1,$caller2,$caller3) ]) }; + } else { + @cdr_ids = map { $_->{id}; } @{ Utils::Rateomat::create_cdrs([ map { + Utils::Rateomat::prepare_cdr($_->{subscriber},undef,$_->{reseller}, + $callee->{subscriber},undef,$callee->{reseller}, + '192.168.0.1',$start_time += 1,$init_secs + $follow_secs); + } ($caller1,$caller2,$caller3) ]) }; + } + + if (ok((scalar @cdr_ids) > 0 && Utils::Rateomat::run_rateomat_threads(),'rate-o-mat executed')) { + my $no_balance = ($balance <= 0.0); + my $prepaid_label = ($prepaid ? ($prepaid_costs ? 'prepaid w prepaid costs, ' : 'prepaid w/o prepaid costs, ') : 'postpaid, '); + my $no_balance_label = ($no_balance ? 'no balance' : 'balance'); + + my $caller_cdr_map = {}; + + ok(Utils::Rateomat::check_cdrs($prepaid_label.$no_balance_label.': ', + map { + my $cdr = Utils::Rateomat::get_cdrs($_); + $caller_cdr_map->{$cdr->{source_account_id}} = $_; + $_ => { id => $_, + rating_status => 'ok', + source_customer_cost => Utils::Rateomat::decimal_to_string((($prepaid || $no_balance) ? $caller_call_costs : 0.0)), + destination_customer_cost => Utils::Rateomat::decimal_to_string((($prepaid || $no_balance) ? $callee_call_costs : 0.0)), + source_reseller_cost => Utils::Rateomat::decimal_to_string($caller_reseller_call_costs), + destination_reseller_cost => Utils::Rateomat::decimal_to_string($callee_reseller_call_costs), + }; + } @cdr_ids + ),'cdrs were all processed'); + + my $label = $prepaid_label.$no_balance_label.', caller 1: '; + my $contract_id = $caller1->{customer}->{id}; + my $cash_balance = 100.0 * $balance - (($no_balance || $prepaid_costs) ? 0.0 : $caller_call_costs); + my $balance_id = Utils::Rateomat::get_cdr_relation_data($label,$caller_cdr_map->{$contract_id},'source','customer','contract_balance_id'); + Utils::Api::check_interval_history($label,$contract_id,[ + { cash => $cash_balance/100.0, + debit => (($no_balance && !$prepaid_costs) ? $caller_call_costs : 0.0)/100.0, + id => $balance_id, + }, + ]); + Utils::Rateomat::check_cdr_cash_balance_data($label,$caller_cdr_map->{$contract_id},'source','customer','cash_balance', + { before => ($prepaid_costs ? $cash_balance + $caller_call_costs : 100.0 * $balance), after => $cash_balance }); + + $label = $prepaid_label.$no_balance_label.', caller 2: '; + $contract_id = $caller2->{customer}->{id}; + $balance_id = Utils::Rateomat::get_cdr_relation_data($label,$caller_cdr_map->{$contract_id},'source','customer','contract_balance_id'); + Utils::Api::check_interval_history($label,$contract_id,[ + { cash => $cash_balance/100.0, + debit => (($no_balance && !$prepaid_costs) ? $caller_call_costs : 0.0)/100.0, + id => $balance_id, + }, + ]); + Utils::Rateomat::check_cdr_cash_balance_data($label,$caller_cdr_map->{$contract_id},'source','customer','cash_balance', + { before => ($prepaid_costs ? $cash_balance + $caller_call_costs : 100.0 * $balance), after => $cash_balance }); + + $label = $prepaid_label.$no_balance_label.', caller 3: '; + $contract_id = $caller3->{customer}->{id}; + $balance_id = Utils::Rateomat::get_cdr_relation_data($label,$caller_cdr_map->{$contract_id},'source','customer','contract_balance_id'); + Utils::Api::check_interval_history($label,$contract_id,[ + { cash => $cash_balance/100.0, + debit => (($no_balance && !$prepaid_costs) ? $caller_call_costs : 0.0)/100.0, + id => $balance_id, + }, + ]); + Utils::Rateomat::check_cdr_cash_balance_data($label,$caller_cdr_map->{$contract_id},'source','customer','cash_balance', + { before => ($prepaid_costs ? $cash_balance + $caller_call_costs : 100.0 * $balance), after => $cash_balance }); + + $label = $prepaid_label.$no_balance_label.', callee: '; + $contract_id = $callee->{customer}->{id}; + $cash_balance = 100.0 * $balance - ($no_balance ? 0.0 : 3 * $callee_call_costs); + $balance_id = Utils::Rateomat::get_cdr_relation_data($label,$cdr_ids[0],'destination','customer','contract_balance_id'); + Utils::Api::check_interval_history($label,$contract_id,[ + { cash => $cash_balance/100.0, + debit => ($no_balance ? 3 * $callee_call_costs : 0.0)/100.0, + id => $balance_id, + }, + ]); + my $bal = 100.0 * $balance; + my $bal_decrease = ($no_balance ? 0.0 : $callee_call_costs); + foreach (@cdr_ids) { # ensure id also orders cdrs by start_time + Utils::Rateomat::check_cdr_relation_data($label,$_,'destination','customer','contract_balance_id',$balance_id); + Utils::Rateomat::check_cdr_cash_balance_data($label,$_,'destination','customer','cash_balance', + { before => $bal, after => $bal - $bal_decrease }); + $bal -= $bal_decrease; + } + + $label = $prepaid_label.$no_balance_label.', callers\' provider: '; + $contract_id = $provider_a->{contract}->{id}; + $balance_id = Utils::Rateomat::get_cdr_relation_data($label,$cdr_ids[0],'source','reseller','contract_balance_id'); + $total_caller_reseller_call_costs += 3 * $caller_reseller_call_costs; + Utils::Api::check_interval_history($label,$contract_id,[ + { debit => $total_caller_reseller_call_costs/100.0, + id => $balance_id, + }, + ]); + foreach (@cdr_ids) { + Utils::Rateomat::check_cdr_relation_data($label,$_,'source','carrier','contract_balance_id',undef); + Utils::Rateomat::check_cdr_relation_data($label,$_,'source','reseller','contract_balance_id',$balance_id); + } + + $label = $prepaid_label.$no_balance_label.', callee\'s provider: '; + $contract_id = $callee->{reseller}->{contract_id}; + $balance_id = Utils::Rateomat::get_cdr_relation_data($label,$cdr_ids[0],'destination','reseller','contract_balance_id'); + $total_callee_reseller_call_costs += 3 * $callee_reseller_call_costs; + Utils::Api::check_interval_history($label,$contract_id,[ + { debit => $total_callee_reseller_call_costs/100.0, + id => $balance_id, + }, + ]); + foreach (@cdr_ids) { + Utils::Rateomat::check_cdr_relation_data($label,$_,'destination','carrier','contract_balance_id',undef); + Utils::Rateomat::check_cdr_relation_data($label,$_,'destination','reseller','contract_balance_id',$balance_id); + } + + $label = $prepaid_label.$no_balance_label.' providers and subscriber must not have a profile package: '; + foreach (@cdr_ids) { + Utils::Rateomat::check_cdr_relation_data($label,$_,'source','carrier','profile_package_id',undef); + Utils::Rateomat::check_cdr_relation_data($label,$_,'source','reseller','profile_package_id',undef); + Utils::Rateomat::check_cdr_relation_data($label,$_,'source','customer','profile_package_id',undef); + Utils::Rateomat::check_cdr_relation_data($label,$_,'destination','carrier','profile_package_id',undef); + Utils::Rateomat::check_cdr_relation_data($label,$_,'destination','reseller','profile_package_id',undef); + Utils::Rateomat::check_cdr_relation_data($label,$_,'destination','customer','profile_package_id',undef); + } + $label = $prepaid_label.$no_balance_label.' providers and subscriber must have zero free time: '; + my $free_time_balance_before_after = { before => 0.0, after => 0.0 }; + foreach (@cdr_ids) { + Utils::Rateomat::check_cdr_time_balance_data($label,$_,'source','carrier','free_time_balance',undef); + Utils::Rateomat::check_cdr_time_balance_data($label,$_,'source','reseller','free_time_balance',$free_time_balance_before_after); + Utils::Rateomat::check_cdr_time_balance_data($label,$_,'source','customer','free_time_balance',$free_time_balance_before_after); + Utils::Rateomat::check_cdr_time_balance_data($label,$_,'destination','carrier','free_time_balance',undef); + Utils::Rateomat::check_cdr_time_balance_data($label,$_,'destination','reseller','free_time_balance',$free_time_balance_before_after); + Utils::Rateomat::check_cdr_time_balance_data($label,$_,'destination','customer','free_time_balance',$free_time_balance_before_after); + } + $label = $prepaid_label.$no_balance_label.' providers must have zero balance: '; + my $cash_balance_before_after = { before => 0.0, after => 0.0 }; + foreach (@cdr_ids) { + Utils::Rateomat::check_cdr_cash_balance_data($label,$_,'source','carrier','cash_balance',undef); + Utils::Rateomat::check_cdr_cash_balance_data($label,$_,'source','reseller','cash_balance',$cash_balance_before_after); + Utils::Rateomat::check_cdr_cash_balance_data($label,$_,'destination','carrier','cash_balance',undef); + Utils::Rateomat::check_cdr_cash_balance_data($label,$_,'destination','reseller','cash_balance',$cash_balance_before_after); + } + } + } + } +} + +done_testing(); +exit; + +sub create_provider { + my $domain = shift; + return Utils::Api::setup_provider($domain, + [ #subscriber rates: + { prepaid => 0, + fees => [{ #outgoing: + direction => 'out', + destination => '^8882.+', + onpeak_init_rate => 6, + onpeak_init_interval => $init_secs, + onpeak_follow_rate => 1, + onpeak_follow_interval => $follow_secs, + offpeak_init_rate => 6, + offpeak_init_interval => $init_secs, + offpeak_follow_rate => 1, + offpeak_follow_interval => $follow_secs, + }, + { #incoming: + direction => 'in', + destination => '.', + source => '^8881.+', + onpeak_init_rate => 5, + onpeak_init_interval => $init_secs, + onpeak_follow_rate => 1, + onpeak_follow_interval => $follow_secs, + offpeak_init_rate => 5, + offpeak_init_interval => $init_secs, + offpeak_follow_rate => 1, + offpeak_follow_interval => $follow_secs, + }]}, + { prepaid => 1, + fees => [{ #outgoing: + direction => 'out', + destination => '^8882.+', + onpeak_init_rate => 4, + onpeak_init_interval => $init_secs, + onpeak_follow_rate => 1, + onpeak_follow_interval => $follow_secs, + offpeak_init_rate => 4, + offpeak_init_interval => $init_secs, + offpeak_follow_rate => 1, + offpeak_follow_interval => $follow_secs, + }, + { #incoming: + direction => 'in', + destination => '.', + source => '^8881.+', + onpeak_init_rate => 3, + onpeak_init_interval => $init_secs, + onpeak_follow_rate => 1, + onpeak_follow_interval => $follow_secs, + offpeak_init_rate => 3, + offpeak_init_interval => $init_secs, + offpeak_follow_rate => 1, + offpeak_follow_interval => $follow_secs, + }]}, + ], + undef, # no billing networks in this test suite + # provider rate: + { prepaid => 0, + fees => [{ #outgoing: + direction => 'out', + destination => '^888.+', + onpeak_init_rate => 2, + onpeak_init_interval => $init_secs, + onpeak_follow_rate => 1, + onpeak_follow_interval => $follow_secs, + offpeak_init_rate => 2, + offpeak_init_interval => $init_secs, + offpeak_follow_rate => 1, + offpeak_follow_interval => $follow_secs, + }, + { #incoming: + direction => 'in', + destination => '.', + source => '^888.+', + onpeak_init_rate => 1, + onpeak_init_interval => $init_secs, + onpeak_follow_rate => 1, + onpeak_follow_interval => $follow_secs, + offpeak_init_rate => 1, + offpeak_init_interval => $init_secs, + offpeak_follow_rate => 1, + offpeak_follow_interval => $follow_secs, + }]}, + ); +} diff --git a/t/rateomat-25-basic-offnet.t b/t/rateomat-25-basic-offnet.t new file mode 100644 index 0000000..6ebdcac --- /dev/null +++ b/t/rateomat-25-basic-offnet.t @@ -0,0 +1,378 @@ +use strict; + +use Utils::Api qw(); +use Utils::Rateomat qw(); +use Test::More; + +### testcase outline: +### prepaid/postpaid calls of onnet callers to offnet callees and +### offnet callers to onnet callees +### +### this tests verify all combinations of prepaid/postpaid subscriber customers with +### balance > 0.0/no balance produce correct customer/reseller call cost, cash balance +### and cash balance interval values. + +my $init_secs = 60; +my $follow_secs = 30; +my @offnet_subscribers = (Utils::Rateomat::prepare_offnet_subsriber_info({ cc => 999, ac => '2', sn => '' },'somewhere.tld'), + Utils::Rateomat::prepare_offnet_subsriber_info({ cc => 999, ac => '2', sn => '' },'somewhere.tld'), + Utils::Rateomat::prepare_offnet_subsriber_info({ cc => 999, ac => '2', sn => '' },'somewhere.tld')); + +#goto SKIP; +{ + + foreach my $ptype (('reseller')) { # 'sippeering' + my $prefix = "onnet $ptype caller calls offnet callees - "; + my $provider = create_subscriber($ptype); + my $is_carrier; + my $provider_type; + if ($ptype ne 'reseller') { + $provider_type = 'carrier'; + $is_carrier = 1; + } else { + $provider_type = 'reseller'; + $is_carrier = 0; + } + my $total_reseller_call_costs = 0.0; + foreach my $prepaid ((0,1)) { # prepaid, postpaid + foreach my $balance ((0.0,30.0)) { # zero balance, enough balance + my $caller_fee = ($prepaid ? $provider->{subscriber_fees}->[1] : $provider->{subscriber_fees}->[0]); + my $caller = Utils::Api::setup_subscriber($provider,$caller_fee->{profile},$balance,{ cc => 888, ac => '1', sn => '' }); + + my $caller_call_costs = $caller_fee->{fees}->[0]->{onpeak_init_rate} * + $caller_fee->{fees}->[0]->{onpeak_init_interval} + + $caller_fee->{fees}->[0]->{onpeak_follow_rate} * + $caller_fee->{fees}->[0]->{onpeak_follow_interval}; + my $caller_reseller_call_costs = $provider->{provider_fee}->{fees}->[0]->{onpeak_init_rate} * + $provider->{provider_fee}->{fees}->[0]->{onpeak_init_interval} + + $provider->{provider_fee}->{fees}->[0]->{onpeak_follow_rate} * + $provider->{provider_fee}->{fees}->[0]->{onpeak_follow_interval}; + + my $callee_call_costs = 0; + my $callee_reseller_call_costs = 0; + + my $start_time = Utils::Api::current_unix() - 5; + my @cdr_ids; + if ($prepaid) { + @cdr_ids = map { $_->{cdr}->{id}; } @{ Utils::Rateomat::create_prepaid_costs_cdrs([ map { + Utils::Rateomat::prepare_prepaid_costs_cdr($caller->{subscriber},undef,$caller->{reseller}, + undef, $_ ,undef, + '192.168.0.1',$start_time += 1,$init_secs + $follow_secs, + $caller_call_costs,0); + } @offnet_subscribers ]) }; + } else { + @cdr_ids = map { $_->{id}; } @{ Utils::Rateomat::create_cdrs([ map { + Utils::Rateomat::prepare_cdr($caller->{subscriber},undef,$caller->{reseller}, + undef, $_ ,undef, + '192.168.0.1',$start_time += 1,$init_secs + $follow_secs); + } @offnet_subscribers ]) }; + } + + if (ok((scalar @cdr_ids) > 0 && Utils::Rateomat::run_rateomat_threads(),'rate-o-mat executed')) { + my $no_balance = ($balance <= 0.0); + my $prepaid_label = ($prepaid ? 'prepaid, ' : 'postpaid, '); + my $no_balance_label = ($no_balance ? 'no balance' : 'balance'); + + ok(Utils::Rateomat::check_cdrs($prefix.$prepaid_label.$no_balance_label.': ', + map { $_ => { + id => $_, + rating_status => 'ok', + source_customer_cost => Utils::Rateomat::decimal_to_string((($prepaid || $no_balance) ? $caller_call_costs : 0.0)), + destination_customer_cost => Utils::Rateomat::decimal_to_string((($prepaid || $no_balance) ? $callee_call_costs : 0.0)), + 'source_'.$provider_type.'_cost' => Utils::Rateomat::decimal_to_string($caller_reseller_call_costs), + 'destination_'.$provider_type.'_cost' => Utils::Rateomat::decimal_to_string($callee_reseller_call_costs), + }; } @cdr_ids + ),'cdrs were all processed'); + + my $label = $prefix.$prepaid_label.$no_balance_label.', caller: '; + my $cash_balance = 100.0 * $balance - (($no_balance || $prepaid) ? 0.0 : 3 * $caller_call_costs); + my $balance_id = Utils::Rateomat::get_cdr_relation_data($label,$cdr_ids[0],'source','customer','contract_balance_id'); + Utils::Api::check_interval_history($label,$caller->{customer}->{id},[ + { cash => $cash_balance/100.0, + debit => (($no_balance && !$prepaid) ? 3 * $caller_call_costs : 0.0)/100.0, + id => $balance_id, + }, + ]); + my $bal = ($prepaid ? $cash_balance + $caller_call_costs : 100.0 * $balance); + my $bal_decrease = (($no_balance && !$prepaid) ? 0.0 : $caller_call_costs); + foreach (@cdr_ids) { # ensure id also orders cdrs by start_time + Utils::Rateomat::check_cdr_relation_data($label,$_,'source','customer','contract_balance_id',$balance_id); + Utils::Rateomat::check_cdr_cash_balance_data($label,$_,'source','customer','cash_balance', + { before => $bal, after => $bal - $bal_decrease }); + $bal -= $bal_decrease if !$prepaid; + } + + $label = $prefix.$prepaid_label.$no_balance_label.', callers\' provider: '; + $balance_id = Utils::Rateomat::get_cdr_relation_data($label,$cdr_ids[0],'source',$provider_type,'contract_balance_id'); + $total_reseller_call_costs += 3 * $caller_reseller_call_costs; + Utils::Api::check_interval_history($label,$provider->{contract}->{id},[ + { debit => $total_reseller_call_costs/100.0, + id => $balance_id, + }, + ]); + + $label = $prefix.$prepaid_label.$no_balance_label.' providers and subscriber must not have a profile package: '; + foreach (@cdr_ids) { + Utils::Rateomat::check_cdr_relation_data($label,$_,'source','carrier','profile_package_id',undef); + Utils::Rateomat::check_cdr_relation_data($label,$_,'source','reseller','profile_package_id',undef); + Utils::Rateomat::check_cdr_relation_data($label,$_,'source','customer','profile_package_id',undef); + Utils::Rateomat::check_cdr_relation_data($label,$_,'destination','carrier','profile_package_id',undef); + Utils::Rateomat::check_cdr_relation_data($label,$_,'destination','reseller','profile_package_id',undef); + Utils::Rateomat::check_cdr_relation_data($label,$_,'destination','customer','profile_package_id',undef); + } + $label = $prefix.$prepaid_label.$no_balance_label.' providers and subscriber must have zero free time: '; + my $free_time_balance_before_after = { before => 0.0, after => 0.0 }; + foreach (@cdr_ids) { + if ($is_carrier) { + Utils::Rateomat::check_cdr_time_balance_data($label,$_,'source','carrier','free_time_balance',$free_time_balance_before_after); + Utils::Rateomat::check_cdr_time_balance_data($label,$_,'source','reseller','free_time_balance',undef); + } else { + Utils::Rateomat::check_cdr_time_balance_data($label,$_,'source','carrier','free_time_balance',undef); + Utils::Rateomat::check_cdr_time_balance_data($label,$_,'source','reseller','free_time_balance',$free_time_balance_before_after); + } + Utils::Rateomat::check_cdr_time_balance_data($label,$_,'source','customer','free_time_balance',$free_time_balance_before_after); + Utils::Rateomat::check_cdr_time_balance_data($label,$_,'destination','carrier','free_time_balance',undef); + Utils::Rateomat::check_cdr_time_balance_data($label,$_,'destination','reseller','free_time_balance',undef); + Utils::Rateomat::check_cdr_time_balance_data($label,$_,'destination','customer','free_time_balance',undef); + } + $label = $prefix.$prepaid_label.$no_balance_label.' providers and destination customers must have zero balance: '; + my $cash_balance_before_after = { before => 0.0, after => 0.0 }; + foreach (@cdr_ids) { + if ($is_carrier) { + Utils::Rateomat::check_cdr_cash_balance_data($label,$_,'source','carrier','cash_balance',$cash_balance_before_after); + Utils::Rateomat::check_cdr_cash_balance_data($label,$_,'source','reseller','cash_balance',undef); + } else { + Utils::Rateomat::check_cdr_cash_balance_data($label,$_,'source','carrier','cash_balance',undef); + Utils::Rateomat::check_cdr_cash_balance_data($label,$_,'source','reseller','cash_balance',$cash_balance_before_after); + } + Utils::Rateomat::check_cdr_cash_balance_data($label,$_,'destination','customer','cash_balance',undef); + Utils::Rateomat::check_cdr_cash_balance_data($label,$_,'destination','carrier','cash_balance',undef); + Utils::Rateomat::check_cdr_cash_balance_data($label,$_,'destination','reseller','cash_balance',undef); + } + + } + } + } + } +} + +#SKIP: +{ + foreach my $ptype (('reseller')) { # 'sippeering' + my $prefix = "offnet callers call onnet $ptype callee - "; + my $provider = create_subscriber($ptype); + my $is_carrier; + my $provider_type; + if ($ptype ne 'reseller') { + $provider_type = 'carrier'; + $is_carrier = 1; + } else { + $provider_type = 'reseller'; + $is_carrier = 0; + } + my $total_reseller_call_costs = 0.0; + foreach my $prepaid ((0,1)) { # prepaid, postpaid + foreach my $balance ((0.0,30.0)) { # zero balance, enough balance + + my $callee_fee = ($prepaid ? $provider->{subscriber_fees}->[1] : $provider->{subscriber_fees}->[0]); + my $callee = Utils::Api::setup_subscriber($provider,$callee_fee->{profile},$balance,{ cc => 888, ac => '2', sn => '' }); + + my $caller_call_costs = 0; + my $caller_reseller_call_costs = 0; + + my $callee_call_costs = $callee_fee->{fees}->[1]->{onpeak_init_rate} * + $callee_fee->{fees}->[1]->{onpeak_init_interval} + + $callee_fee->{fees}->[1]->{onpeak_follow_rate} * + $callee_fee->{fees}->[1]->{onpeak_follow_interval}; + my $callee_reseller_call_costs = $provider->{provider_fee}->{fees}->[1]->{onpeak_init_rate} * + $provider->{provider_fee}->{fees}->[1]->{onpeak_init_interval} + + $provider->{provider_fee}->{fees}->[1]->{onpeak_follow_rate} * + $provider->{provider_fee}->{fees}->[1]->{onpeak_follow_interval}; + + my $start_time = Utils::Api::current_unix() - 5; + my @cdr_ids = map { $_->{id}; } @{ Utils::Rateomat::create_cdrs([ map { + Utils::Rateomat::prepare_cdr(undef, $_ ,undef, + $callee->{subscriber},undef,$callee->{reseller}, + '192.168.0.1',$start_time += 1,$init_secs + $follow_secs); + } @offnet_subscribers ]) }; + + if (ok((scalar @cdr_ids) > 0 && Utils::Rateomat::run_rateomat_threads(),'rate-o-mat executed')) { + my $no_balance = ($balance <= 0.0); + my $prepaid_label = ($prepaid ? 'prepaid, ' : 'postpaid, '); + my $no_balance_label = ($no_balance ? 'no balance' : 'balance'); + + ok(Utils::Rateomat::check_cdrs($prefix.$prepaid_label.$no_balance_label.': ', + map { $_ => { + id => $_, + rating_status => 'ok', + source_customer_cost => Utils::Rateomat::decimal_to_string($caller_call_costs), + destination_customer_cost => Utils::Rateomat::decimal_to_string((($prepaid || $no_balance) ? $callee_call_costs : 0.0)), + 'source_'.$provider_type.'_cost' => Utils::Rateomat::decimal_to_string($caller_reseller_call_costs), + 'destination_'.$provider_type.'_cost' => Utils::Rateomat::decimal_to_string($callee_reseller_call_costs), + }; } @cdr_ids + ),'cdrs were all processed'); + + my $label = $prefix.$prepaid_label.$no_balance_label.', callee: '; + my $cash_balance = 100.0 * $balance - ($no_balance ? 0.0 : 3 * $callee_call_costs); + my $balance_id = Utils::Rateomat::get_cdr_relation_data($label,$cdr_ids[0],'destination','customer','contract_balance_id'); + Utils::Api::check_interval_history($label,$callee->{customer}->{id},[ + { cash => $cash_balance/100.0, + debit => ($no_balance ? 3 * $callee_call_costs : 0.0)/100.0, + id => $balance_id, + }, + ]); + my $bal = 100.0 * $balance; + my $bal_decrease = ($no_balance ? 0.0 : $callee_call_costs); + foreach (@cdr_ids) { # ensure id also orders cdrs by start_time + Utils::Rateomat::check_cdr_relation_data($label,$_,'destination','customer','contract_balance_id',$balance_id); + Utils::Rateomat::check_cdr_cash_balance_data($label,$_,'destination','customer','cash_balance', + { before => $bal, after => $bal - $bal_decrease }); + $bal -= $bal_decrease; + } + + $label = $prefix.$prepaid_label.$no_balance_label.', callee\'s provider: '; + $balance_id = Utils::Rateomat::get_cdr_relation_data($label,$cdr_ids[0],'destination',$provider_type,'contract_balance_id'); + $total_reseller_call_costs += 3 * $callee_reseller_call_costs; + Utils::Api::check_interval_history($label,$callee->{reseller}->{contract_id},[ + { debit => $total_reseller_call_costs/100.0, + id => $balance_id, + }, + ]); + + $label = $prefix.$prepaid_label.$no_balance_label.' providers and subscriber must not have a profile package: '; + foreach (@cdr_ids) { + Utils::Rateomat::check_cdr_relation_data($label,$_,'source','carrier','profile_package_id',undef); + Utils::Rateomat::check_cdr_relation_data($label,$_,'source','reseller','profile_package_id',undef); + Utils::Rateomat::check_cdr_relation_data($label,$_,'source','customer','profile_package_id',undef); + Utils::Rateomat::check_cdr_relation_data($label,$_,'destination','carrier','profile_package_id',undef); + Utils::Rateomat::check_cdr_relation_data($label,$_,'destination','reseller','profile_package_id',undef); + Utils::Rateomat::check_cdr_relation_data($label,$_,'destination','customer','profile_package_id',undef); + } + $label = $prefix.$prepaid_label.$no_balance_label.' providers and subscriber must have zero free time: '; + my $free_time_balance_before_after = { before => 0.0, after => 0.0 }; + foreach (@cdr_ids) { + if ($is_carrier) { + Utils::Rateomat::check_cdr_time_balance_data($label,$_,'destination','carrier','free_time_balance',$free_time_balance_before_after); + Utils::Rateomat::check_cdr_time_balance_data($label,$_,'destination','reseller','free_time_balance',undef); + } else { + Utils::Rateomat::check_cdr_time_balance_data($label,$_,'destination','carrier','free_time_balance',undef); + Utils::Rateomat::check_cdr_time_balance_data($label,$_,'destination','reseller','free_time_balance',$free_time_balance_before_after); + } + Utils::Rateomat::check_cdr_time_balance_data($label,$_,'destination','customer','free_time_balance',$free_time_balance_before_after); + Utils::Rateomat::check_cdr_time_balance_data($label,$_,'source','carrier','free_time_balance',undef); + Utils::Rateomat::check_cdr_time_balance_data($label,$_,'source','reseller','free_time_balance',undef); + Utils::Rateomat::check_cdr_time_balance_data($label,$_,'source','customer','free_time_balance',undef); + } + $label = $prefix.$prepaid_label.$no_balance_label.' providers and destination customers must have zero balance: '; + my $cash_balance_before_after = { before => 0.0, after => 0.0 }; + foreach (@cdr_ids) { + if ($is_carrier) { + Utils::Rateomat::check_cdr_cash_balance_data($label,$_,'destination','carrier','cash_balance',$cash_balance_before_after); + Utils::Rateomat::check_cdr_cash_balance_data($label,$_,'destination','reseller','cash_balance',undef); + } else { + Utils::Rateomat::check_cdr_cash_balance_data($label,$_,'destination','carrier','cash_balance',undef); + Utils::Rateomat::check_cdr_cash_balance_data($label,$_,'destination','reseller','cash_balance',$cash_balance_before_after); + } + Utils::Rateomat::check_cdr_cash_balance_data($label,$_,'source','customer','cash_balance',undef); + Utils::Rateomat::check_cdr_cash_balance_data($label,$_,'source','carrier','cash_balance',undef); + Utils::Rateomat::check_cdr_cash_balance_data($label,$_,'source','reseller','cash_balance',undef); + } + + } + + } + } + } +} + +done_testing(); +exit; + +sub create_subscriber { + my $type = shift; + return Utils::Api::setup_provider('test.com', + [ #subscriber rates: + { prepaid => 0, + fees => [{ #outgoing: + direction => 'out', + destination => '^999.+', + onpeak_init_rate => 6, + onpeak_init_interval => $init_secs, + onpeak_follow_rate => 1, + onpeak_follow_interval => $follow_secs, + offpeak_init_rate => 6, + offpeak_init_interval => $init_secs, + offpeak_follow_rate => 1, + offpeak_follow_interval => $follow_secs, + }, + { #incoming: + direction => 'in', + destination => '.', + source => '^999.+', + onpeak_init_rate => 5, + onpeak_init_interval => $init_secs, + onpeak_follow_rate => 1, + onpeak_follow_interval => $follow_secs, + offpeak_init_rate => 5, + offpeak_init_interval => $init_secs, + offpeak_follow_rate => 1, + offpeak_follow_interval => $follow_secs, + }]}, + { prepaid => 1, + fees => [{ #outgoing: + direction => 'out', + destination => '^999.+', + onpeak_init_rate => 4, + onpeak_init_interval => $init_secs, + onpeak_follow_rate => 1, + onpeak_follow_interval => $follow_secs, + offpeak_init_rate => 4, + offpeak_init_interval => $init_secs, + offpeak_follow_rate => 1, + offpeak_follow_interval => $follow_secs, + }, + { #incoming: + direction => 'in', + destination => '.', + source => '^999.+', + onpeak_init_rate => 3, + onpeak_init_interval => $init_secs, + onpeak_follow_rate => 1, + onpeak_follow_interval => $follow_secs, + offpeak_init_rate => 3, + offpeak_init_interval => $init_secs, + offpeak_follow_rate => 1, + offpeak_follow_interval => $follow_secs, + }]}, + ], + undef, # no billing networks in this test suite + # provider rate: + { prepaid => 0, + fees => [{ #outgoing: + direction => 'out', + destination => '^999.+', + onpeak_init_rate => 2, + onpeak_init_interval => $init_secs, + onpeak_follow_rate => 1, + onpeak_follow_interval => $follow_secs, + offpeak_init_rate => 2, + offpeak_init_interval => $init_secs, + offpeak_follow_rate => 1, + offpeak_follow_interval => $follow_secs, + }, + { #incoming: + direction => 'in', + destination => '.', + source => '^999.+', + onpeak_init_rate => 1, + onpeak_init_interval => $init_secs, + onpeak_follow_rate => 1, + onpeak_follow_interval => $follow_secs, + offpeak_init_rate => 1, + offpeak_init_interval => $init_secs, + offpeak_follow_rate => 1, + offpeak_follow_interval => $follow_secs, + }]}, + $type + ); +} diff --git a/t/rateomat-negative-fees.t b/t/rateomat-30-negative-fees.t similarity index 87% rename from t/rateomat-negative-fees.t rename to t/rateomat-30-negative-fees.t index 13fce2a..5923d5d 100644 --- a/t/rateomat-negative-fees.t +++ b/t/rateomat-30-negative-fees.t @@ -5,6 +5,14 @@ use Utils::Api qw(); use Utils::Rateomat qw(); use Test::More; +### testcase outline: +### onnet calls that hit negative incoming fees, aka VAS +### (value added services) numbers +### +### this tests verify that rating with negative rates +### properly increase the destination customer's cash +### balance. + use Text::Table; use Text::Wrap; use Storable; @@ -62,15 +70,15 @@ my $tb = Text::Table->new("request", "response"); '192.168.0.1',$now->epoch,61), ]) }; - if (ok((scalar @cdr_ids) > 0 && Utils::Rateomat::run_rateomat(),'rate-o-mat executed')) { + if (ok((scalar @cdr_ids) > 0 && Utils::Rateomat::run_rateomat_threads(),'rate-o-mat executed')) { ok(Utils::Rateomat::check_cdrs('', map { $_ => { id => $_, rating_status => 'ok', }; } @cdr_ids ),'cdrs were all processed'); - Utils::Api::check_interval_history('negative fees - caller',$caller->{customer}->{id},[ + Utils::Api::check_interval_history('negative fees - caller: ',$caller->{customer}->{id},[ { cash => $balance - $caller_costs, profile => $provider->{subscriber_fees}->[0]->{profile}->{id} }, ]); - Utils::Api::check_interval_history('negative fees - callee',$callee->{customer}->{id},[ + Utils::Api::check_interval_history('negative fees - callee: ',$callee->{customer}->{id},[ { cash => $balance - $callee_costs, profile => $provider->{subscriber_fees}->[0]->{profile}->{id} }, ]); @@ -78,7 +86,7 @@ my $tb = Text::Table->new("request", "response"); } -print $tb->stringify; +#print $tb->stringify; done_testing(); exit; @@ -102,6 +110,7 @@ sub create_provider { { #negative: direction => 'in', destination => '.', + source => '.', onpeak_init_rate => -1*2, onpeak_init_interval => 60, onpeak_follow_rate => -1*1, diff --git a/t/rateomat-33-freetime.t b/t/rateomat-33-freetime.t new file mode 100644 index 0000000..5bffc2b --- /dev/null +++ b/t/rateomat-33-freetime.t @@ -0,0 +1,121 @@ + +use strict; + +use Utils::Api qw(); +use Utils::Rateomat qw(); +use Test::More; + +### testcase outline: +### onnet calls that consume profile's freetime +### +### this tests verify that rating correctly +### consumes up free time before cash balance. + +my $init_secs = 50; +my $follow_secs = 20; +my $in_free_time = $init_secs + 20*$follow_secs; +my $out_free_time = $init_secs + 30*$follow_secs; + +{ + my $now = Utils::Api::get_now(); + my $begin = $now->clone->truncate(to => 'month'); # prevent ratio + Utils::Api::set_time($begin); + my $provider = create_provider($in_free_time,$out_free_time); + my $balance = 5; + my $caller = Utils::Api::setup_subscriber($provider,$provider->{subscriber_fees}->[0]->{profile},$balance,{ cc => 888, ac => '1', sn => '' }); + my $callee = Utils::Api::setup_subscriber($provider,$provider->{subscriber_fees}->[1]->{profile},$balance,{ cc => 888, ac => '2', sn => '' }); + my $caller_costs = 2 * ($provider->{subscriber_fees}->[0]->{fees}->[0]->{onpeak_follow_rate} * + $provider->{subscriber_fees}->[0]->{fees}->[0]->{onpeak_follow_interval})/100.0; + my $callee_costs = 2 * ($provider->{subscriber_fees}->[1]->{fees}->[0]->{onpeak_follow_rate} * + $provider->{subscriber_fees}->[1]->{fees}->[0]->{onpeak_follow_interval})/100.0; + + my $max_free_time = ($in_free_time, $out_free_time)[$in_free_time < $out_free_time]; + my $call_duration = $follow_secs + $max_free_time + 1; + Utils::Api::set_time($begin->clone->add(seconds => $call_duration + 20)); + my $start_time = Utils::Api::current_unix() - $call_duration - 1; + Utils::Api::set_time(); + my @cdr_ids = map { $_->{id}; } @{ Utils::Rateomat::create_cdrs([ + Utils::Rateomat::prepare_cdr($caller->{subscriber},undef,$caller->{reseller}, + $callee->{subscriber},undef,$callee->{reseller}, + '192.168.0.1',$start_time,$call_duration), + ]) }; + + if (ok((scalar @cdr_ids) > 0 && Utils::Rateomat::run_rateomat_threads(),'rate-o-mat executed')) { + ok(Utils::Rateomat::check_cdrs('', + map { $_ => { + id => $_, + rating_status => 'ok', + source_customer_free_time => $out_free_time, + destination_customer_free_time => $in_free_time, + }; } @cdr_ids + ),'cdrs were all processed'); + my $label = 'freetime - caller: '; + my $balance_id = Utils::Rateomat::get_cdr_relation_data($label,$cdr_ids[0],'source','customer','contract_balance_id'); + Utils::Api::check_interval_history($label,$caller->{customer}->{id},[ + { (($in_free_time < $out_free_time) ? (cash => $balance - $caller_costs) : ()), + profile => $provider->{subscriber_fees}->[0]->{profile}->{id}, + id => $balance_id, + }, + ]); + Utils::Rateomat::check_cdr_time_balance_data($label,$cdr_ids[0],'source','customer','free_time_balance', + { before => $out_free_time, after => 0 }); + $label = 'freetime - callee: '; + $balance_id = Utils::Rateomat::get_cdr_relation_data($label,$cdr_ids[0],'destination','customer','contract_balance_id'); + Utils::Api::check_interval_history($label,$callee->{customer}->{id},[ + { (($in_free_time > $out_free_time) ? (cash => $balance - $callee_costs) : ()), + profile => $provider->{subscriber_fees}->[1]->{profile}->{id}, + id => $balance_id, + }, + ]); + Utils::Rateomat::check_cdr_time_balance_data($label,$cdr_ids[0],'destination','customer','free_time_balance', + { before => $in_free_time, after => 0 }); + } + +} + +done_testing(); +exit; + +sub create_provider { + my ($free_time_in,$free_time_out) = @_; + return Utils::Api::setup_provider('test.com', + [ #rates: + { interval_free_time => $free_time_out, + fees => [ + { + direction => 'out', + destination => '.', + onpeak_init_rate => 3, + onpeak_init_interval => $init_secs, + onpeak_follow_rate => 3, + onpeak_follow_interval => $follow_secs, + offpeak_init_rate => 2, + offpeak_init_interval => $init_secs, + offpeak_follow_rate => 2, + offpeak_follow_interval => $follow_secs, + use_free_time => 1, + }, + ]}, + { interval_free_time => $free_time_in, + fees => [ + { + direction => 'in', + destination => '.', + source => '.', + onpeak_init_rate => 4, + onpeak_init_interval => $init_secs, + onpeak_follow_rate => 4, + onpeak_follow_interval => $follow_secs, + offpeak_init_rate => 2, + offpeak_init_interval => $init_secs, + offpeak_follow_rate => 2, + offpeak_follow_interval => $follow_secs, + interval_free_time => $free_time_in, + use_free_time => 1, + }, + ]}, + ], + [ #billing networks: + ] + ); +} diff --git a/t/rateomat-roaming.t b/t/rateomat-35-roaming.t similarity index 83% rename from t/rateomat-roaming.t rename to t/rateomat-35-roaming.t index 8f3f5be..9e646be 100644 --- a/t/rateomat-roaming.t +++ b/t/rateomat-35-roaming.t @@ -5,25 +5,12 @@ use Utils::Api qw(); use Utils::Rateomat qw(); use Test::More; -#$ENV{CATALYST_SERVER} = https://127.0.0.1:4443 -#$ENV{RATEOMAT_PL} = /home/rkrenn/sipwise/git/rate-o-mat/rate-o-mat.pl - -#$ENV{CATALYST_SERVER} -#$ENV{API_USER} -#$ENV{API_USER} -#$ENV{RATEOMAT_PROVISIONING_DB_HOST} -#$ENV{RATEOMAT_PROVISIONING_DB_PORT} -#$ENV{RATEOMAT_PROVISIONING_DB_USER} -#$ENV{RATEOMAT_PROVISIONING_DB_PASS} -#$ENV{RATEOMAT_BILLING_DB_HOST} -#$ENV{RATEOMAT_BILLING_DB_PORT} -#$ENV{RATEOMAT_BILLING_DB_USER} -#$ENV{RATEOMAT_BILLING_DB_PASS} -#$ENV{RATEOMAT_ACCOUNTING_DB_HOST} -#$ENV{RATEOMAT_ACCOUNTING_DB_PORT} -#$ENV{RATEOMAT_ACCOUNTING_DB_USER} -#$ENV{RATEOMAT_ACCOUNTING_DB_PASS} -#$ENV{RATEOMAT_PL} +### testcase outline: +### onnet calls of callers with profile + billing +### network billing mappings +### +### this tests verify that rates are correctly choosen +### depending on the caller (source) ip. my $provider = Utils::Api::setup_provider('test.com', [ #rates: @@ -95,7 +82,7 @@ my @cdr_ids = map { $_->{id}; } @{ Utils::Rateomat::create_cdrs([ '10.0.0.97',Utils::Api::current_unix(),1), ]) }; -if (ok((scalar @cdr_ids) > 0 && Utils::Rateomat::run_rateomat(),'rate-o-mat executed')) { +if (ok((scalar @cdr_ids) > 0 && Utils::Rateomat::run_rateomat_threads(),'rate-o-mat executed')) { ok(Utils::Rateomat::check_cdrs('', $cdr_ids[0] => { id => $cdr_ids[0], diff --git a/t/rateomat-catchup-intervals.t b/t/rateomat-40-catchup-intervals.t similarity index 94% rename from t/rateomat-catchup-intervals.t rename to t/rateomat-40-catchup-intervals.t index 9d9e8cc..54b0da5 100644 --- a/t/rateomat-catchup-intervals.t +++ b/t/rateomat-40-catchup-intervals.t @@ -5,6 +5,18 @@ use Utils::Api qw(); use Utils::Rateomat qw(); use Test::More; use Data::Dumper; + +### testcase outline: +### onnet calls of a caller with and without profile packages to check +### the correct creation of billing.contract_balance records ("catchup") +### +### the tests verify, that a correct history of balance interval records +### (contract_balance records) are created while rating a cdr. beside the +### default case of customer with no package, all combinations of interval +### start modes and interval duration units are covered. +### note: since it also includes minute-based balance intervals, this tests +### takes longer time to complete + #goto SKIP; { #no package: my $now = Utils::Api::get_now(); @@ -34,7 +46,7 @@ use Data::Dumper; '192.168.0.1',$now->epoch,1), ]) }; - if (ok((scalar @cdr_ids) > 0 && Utils::Rateomat::run_rateomat(),'rate-o-mat executed')) { + if (ok((scalar @cdr_ids) > 0 && Utils::Rateomat::run_rateomat_threads(),'rate-o-mat executed')) { ok(Utils::Rateomat::check_cdrs('', $cdr_ids[0] => { id => $cdr_ids[0], @@ -126,7 +138,7 @@ use Data::Dumper; '192.168.0.1',$last_call_ts->epoch,1), ]) }; - if (ok((scalar @cdr_ids) > 0 && Utils::Rateomat::run_rateomat(),'rate-o-mat executed')) { + if (ok((scalar @cdr_ids) > 0 && Utils::Rateomat::run_rateomat_threads(),'rate-o-mat executed')) { ok(Utils::Rateomat::check_cdrs('', $cdr_ids[0] => { id => $cdr_ids[0], @@ -242,7 +254,7 @@ use Data::Dumper; '192.168.0.1',$call_ts->epoch,$call_minutes*60), ]) }; - if (ok((scalar @cdr_ids) > 0 && Utils::Rateomat::run_rateomat(),'rate-o-mat executed')) { + if (ok((scalar @cdr_ids) > 0 && Utils::Rateomat::run_rateomat_threads(),'rate-o-mat executed')) { ok(Utils::Rateomat::check_cdrs('', $cdr_ids[0] => { id => $cdr_ids[0], @@ -315,7 +327,7 @@ use Data::Dumper; '192.168.0.1',$call_ts->epoch,$call_minutes*60), ]) }; - if (ok((scalar @cdr_ids) > 0 && Utils::Rateomat::run_rateomat(),'rate-o-mat executed')) { + if (ok((scalar @cdr_ids) > 0 && Utils::Rateomat::run_rateomat_threads(),'rate-o-mat executed')) { ok(Utils::Rateomat::check_cdrs('', $cdr_ids[0] => { id => $cdr_ids[0], diff --git a/t/rateomat-balanceunderrun.t b/t/rateomat-41-balanceunderrun.t similarity index 92% rename from t/rateomat-balanceunderrun.t rename to t/rateomat-41-balanceunderrun.t index 72f20db..a8ea09b 100644 --- a/t/rateomat-balanceunderrun.t +++ b/t/rateomat-41-balanceunderrun.t @@ -5,6 +5,14 @@ use Utils::Api qw(); use Utils::Rateomat qw(); use Test::More; +### testcase outline: +### onnet calls of a caller with profile packages specifying "underrun" +### settings. +### +### the tests verify, that subscriber underrun lock levels and underrun +### profile are correctly applied when balance was discarded during catchup, +### or call costs decrease the balance so it drops below the thresholds. + Utils::Api::set_time(Utils::Api::get_now->subtract(months => 5)); #provider contract needs to be created in the past as well: my $provider = create_provider(); @@ -56,7 +64,7 @@ my $lock_level = 4; '192.168.0.1',$now->epoch,1), ]) }; - if (ok((scalar @cdr_ids) > 0 && Utils::Rateomat::run_rateomat(),'rate-o-mat executed')) { + if (ok((scalar @cdr_ids) > 0 && Utils::Rateomat::run_rateomat_threads(),'rate-o-mat executed')) { ok(Utils::Rateomat::check_cdrs('', map { $_ => { id => $_, rating_status => 'ok', }; } @cdr_ids ),'cdrs were all processed'); @@ -115,7 +123,7 @@ my $lock_level = 4; '192.168.0.1',$now->epoch,1), ]) }; - if (ok((scalar @cdr_ids) > 0 && Utils::Rateomat::run_rateomat(),'rate-o-mat executed')) { + if (ok((scalar @cdr_ids) > 0 && Utils::Rateomat::run_rateomat_threads(),'rate-o-mat executed')) { ok(Utils::Rateomat::check_cdrs('', map { $_ => { id => $_, rating_status => 'ok', }; } @cdr_ids ),'cdrs were all processed'); diff --git a/t/rateomat-catchup-discard.t b/t/rateomat-42-catchup-discard.t similarity index 92% rename from t/rateomat-catchup-discard.t rename to t/rateomat-42-catchup-discard.t index 3518878..04c0be9 100644 --- a/t/rateomat-catchup-discard.t +++ b/t/rateomat-42-catchup-discard.t @@ -6,6 +6,15 @@ use Utils::Rateomat qw(); use Test::More; use Storable qw(); +### testcase outline: +### onnet calls of a caller with profile packages specifying settings to +### discard the cash balance. +### +### the tests verify, that balance is properly discarded (set to 0 euro) +### for all combinations of interval start modes and carry over modes, +### which also depends on topups performed. +### note: this tests takes longer time to complete + Utils::Api::set_time(Utils::Api::get_now->subtract(months => 5)); #provider contract needs to be created in the past as well: my $provider = create_provider(); @@ -54,8 +63,8 @@ foreach my $start_mode ('create','1st') { Utils::Api::set_time($within_first_interval); Utils::Api::perform_topup($caller_topup->{subscriber},$amount); - my $within_first_topup = $begin->clone->add(days => ($interval_days - $timely_days / 2)); - Utils::Api::set_time($within_first_topup); + my $within_first_timely = $begin->clone->add(days => ($interval_days - $timely_days / 2)); + Utils::Api::set_time($within_first_timely); Utils::Api::perform_topup($caller_timelytopup->{subscriber},$amount); Utils::Api::set_time(); @@ -85,7 +94,7 @@ foreach my $start_mode ('create','1st') { '192.168.0.1',$now->epoch,1), ]) }; - if (ok((scalar @cdr_ids) > 0 && Utils::Rateomat::run_rateomat(),'rate-o-mat executed')) { + if (ok((scalar @cdr_ids) > 0 && Utils::Rateomat::run_rateomat_threads(),'rate-o-mat executed')) { ok(Utils::Rateomat::check_cdrs('', map { $_ => { id => $_, rating_status => 'ok', }; } @cdr_ids ),'cdrs were all processed'); @@ -96,12 +105,14 @@ foreach my $start_mode ('create','1st') { stop => Utils::Api::datetime_to_string($begin->add(days => $interval_days)->clone->subtract(seconds => 1)), }; } 1..4; if ('carry_over' eq $carry_over_mode) { - Utils::Api::check_interval_history($label . ' no topup: ',$caller_notopup->{customer}->{id},[ + if (not Utils::Api::check_interval_history($label . ' no topup: ',$caller_notopup->{customer}->{id},[ set_cash($intervals[0],$amount - $costs), set_cash($intervals[1],$amount - $costs), set_cash($intervals[2],0), set_cash($intervals[3],0), - ]); + ]) ){ + print "FAIL due to bug"; + } Utils::Api::check_interval_history($label . ' topup: ',$caller_topup->{customer}->{id},[ set_cash($intervals[0],2*$amount - $costs), set_cash($intervals[1],2*$amount - $costs), @@ -209,7 +220,7 @@ foreach my $carry_over_mode ('carry_over','carry_over_timely') { '192.168.0.1',$call_time->epoch,1), ]) }; - if (ok((scalar @cdr_ids) > 0 && Utils::Rateomat::run_rateomat(),'rate-o-mat executed')) { + if (ok((scalar @cdr_ids) > 0 && Utils::Rateomat::run_rateomat_threads(),'rate-o-mat executed')) { ok(Utils::Rateomat::check_cdrs('', map { $_ => { id => $_, rating_status => 'ok', }; } @cdr_ids ),'cdrs were all processed'); diff --git a/t/rateomat-dst-transitions.t b/t/rateomat-43-dst-transitions.t similarity index 93% rename from t/rateomat-dst-transitions.t rename to t/rateomat-43-dst-transitions.t index 48c3979..88d8265 100644 --- a/t/rateomat-dst-transitions.t +++ b/t/rateomat-43-dst-transitions.t @@ -5,6 +5,13 @@ use Utils::Api qw(); use Utils::Rateomat qw(); use Test::More; +### testcase outline: +### first onnet calls of a caller with hourly-based balance intervals starting +### after DST (daylight saving time) barriers +### +### this short tests verify that created contract_balance records show a +### correct gap in their hourly balance intervals. + if ('Europe/Vienna' eq Utils::Api::get_now->time_zone->name) { Utils::Api::set_time(Utils::Api::datetime_from_string('2015-03-01 00:00:00')); @@ -49,7 +56,7 @@ if ('Europe/Vienna' eq Utils::Api::get_now->time_zone->name) { '192.168.0.1',$now->epoch,1), ]) }; - if (ok((scalar @cdr_ids) > 0 && Utils::Rateomat::run_rateomat(),'rate-o-mat executed')) { + if (ok((scalar @cdr_ids) > 0 && Utils::Rateomat::run_rateomat_threads(),'rate-o-mat executed')) { ok(Utils::Rateomat::check_cdrs('', map { $_ => { id => $_, rating_status => 'ok', }; } @cdr_ids ),'cdrs were all processed'); @@ -81,7 +88,7 @@ if ('Europe/Vienna' eq Utils::Api::get_now->time_zone->name) { '192.168.0.1',$now->epoch,1), ]) }; - if (ok((scalar @cdr_ids) > 0 && Utils::Rateomat::run_rateomat(),'rate-o-mat executed')) { + if (ok((scalar @cdr_ids) > 0 && Utils::Rateomat::run_rateomat_threads(),'rate-o-mat executed')) { ok(Utils::Rateomat::check_cdrs('', map { $_ => { id => $_, rating_status => 'ok', }; } @cdr_ids ),'cdrs were all processed'); diff --git a/t/rateomat-split-cdr.t b/t/rateomat-45-split-cdr.t similarity index 94% rename from t/rateomat-split-cdr.t rename to t/rateomat-45-split-cdr.t index 30b5a06..50ac795 100644 --- a/t/rateomat-split-cdr.t +++ b/t/rateomat-45-split-cdr.t @@ -5,6 +5,15 @@ use Utils::Api qw(); use Utils::Rateomat qw(); use Test::More; +### testcase outline: +### onnet calls of callers with profiles using different +### onpeak/offpeak rates +### +### this tests verify that offpeak/onpeak rates are correctly +### chosen depending call start time. for alternating offpeak/onpeak +### phases during a single call, another new cdr has to be created +### per peaktime fragment with each rateomat loop ("split peak parts"). + $ENV{RATEOMAT_SPLIT_PEAK_PARTS} = 1; #use Text::Table; @@ -70,7 +79,7 @@ $ENV{RATEOMAT_SPLIT_PEAK_PARTS} = 1; my %cdr_id_map = (); my $onpeak = 0; #call starts offpeak my $i = 1; - while (defined $cdr && ok(Utils::Rateomat::run_rateomat(),'rate-o-mat executed')) { + while (defined $cdr && ok(Utils::Rateomat::run_rateomat_threads(),'rate-o-mat executed')) { $cdr = Utils::Rateomat::get_cdrs($cdr->{id}); $cdr_id_map{$cdr->{id}} = $cdr; Utils::Rateomat::check_cdr('cdr was processed: ',$cdr->{id},{ rating_status => 'ok' }); diff --git a/t/add_interval.t b/t/rateomat-99-add_interval.t similarity index 89% rename from t/add_interval.t rename to t/rateomat-99-add_interval.t index b8f07bb..2f33b95 100644 --- a/t/add_interval.t +++ b/t/rateomat-99-add_interval.t @@ -2,9 +2,14 @@ use strict; #use Time::Local qw(timegm timelocal); use POSIX qw(mktime); - use Test::More; +### testcase outline: +### unit test for rateomat's add_interval sub +### +### this tests check the add_interval method in detail, +### in particular mktime's rollover with args like month=99 + my $t1 = '2015-99-99 99:99:99'; my $t2 = from_epoch(to_epoch($t1)); is($t2,'2023-06-11 04:40:39'); @@ -29,10 +34,10 @@ sub to_epoch { } sub from_epoch { - + my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(shift); return sprintf "%4d-%02d-%02d %02d:%02d:%02d",$year+1900,$mon+1,$mday,$hour,$min,$sec; - + } sub add_interval { @@ -40,11 +45,11 @@ sub add_interval { my $to_time; my ($from_year,$from_month,$from_day,$from_hour,$from_minute,$from_second) = (localtime($from_time))[5,4,3,2,1,0]; if($unit eq "minute") { - $to_time = mktime($from_second,$from_minute + $count,$from_hour,$from_day,$from_month,$from_year); + $to_time = mktime($from_second,$from_minute + $count,$from_hour,$from_day,$from_month,$from_year); } elsif($unit eq "hour") { $to_time = mktime($from_second,$from_minute,$from_hour + $count,$from_day,$from_month,$from_year); } elsif($unit eq "day") { - $to_time = mktime($from_second,$from_minute,$from_hour,$from_day + $count,$from_month,$from_year); + $to_time = mktime($from_second,$from_minute,$from_hour,$from_day + $count,$from_month,$from_year); } elsif($unit eq "week") { $to_time = mktime($from_second,$from_minute,$from_hour,$from_day + 7*$count,$from_month,$from_year); } elsif($unit eq "month") { @@ -65,4 +70,4 @@ sub add_interval { die("Invalid interval unit '$unit' in $src"); } return $to_time; -} \ No newline at end of file +} diff --git a/t/rateomat-parallel.t b/t/rateomat-parallel.t deleted file mode 100644 index e93f8a2..0000000 --- a/t/rateomat-parallel.t +++ /dev/null @@ -1,74 +0,0 @@ - -use strict; - -use Utils::Api qw(); -use Utils::Rateomat qw(); -use Test::More; - -{ - - Utils::Rateomat::run_rateomat_threads(3,6); - -} - -#{ -# my $provider = create_provider(); -# my $profile = $provider->{subscriber_fees}->[0]->{profile}; -# my $balance = 5; -# my $caller = Utils::Api::setup_subscriber($provider,$profile,$balance,{ cc => 888, ac => '1', sn => '' }); -# my $callee = Utils::Api::setup_subscriber($provider,$profile,$balance,{ cc => 888, ac => '2', sn => '' }); -# my $caller_costs = ($provider->{subscriber_fees}->[0]->{fees}->[0]->{onpeak_init_rate} * -# $provider->{subscriber_fees}->[0]->{fees}->[0]->{onpeak_init_interval} + -# $provider->{subscriber_fees}->[0]->{fees}->[0]->{onpeak_follow_rate} * -# $provider->{subscriber_fees}->[0]->{fees}->[0]->{onpeak_follow_interval})/100.0; -# my $callee_costs = ($provider->{subscriber_fees}->[0]->{fees}->[1]->{onpeak_init_rate} * -# $provider->{subscriber_fees}->[0]->{fees}->[1]->{onpeak_init_interval} + -# $provider->{subscriber_fees}->[0]->{fees}->[1]->{onpeak_follow_rate} * -# $provider->{subscriber_fees}->[0]->{fees}->[1]->{onpeak_follow_interval})/100.0; #negative! -# -# my $now = Utils::Api::get_now(); -# my @cdr_ids = map { $_->{id}; } @{ Utils::Rateomat::create_cdrs([ -# Utils::Rateomat::prepare_cdr($caller->{subscriber},undef,$caller->{reseller}, -# $callee->{subscriber},undef,$callee->{reseller}, -# '192.168.0.1',$now->epoch,61), -# ]) }; -# -# if (ok((scalar @cdr_ids) > 0 && Utils::Rateomat::run_rateomat(),'rate-o-mat executed')) { -# ok(Utils::Rateomat::check_cdrs('', -# map { $_ => { id => $_, rating_status => 'ok', }; } @cdr_ids -# ),'cdrs were all processed'); -# Utils::Api::check_interval_history('negative fees - caller',$caller->{customer}->{id},[ -# { cash => $balance - $caller_costs, -# profile => $provider->{subscriber_fees}->[0]->{profile}->{id} }, -# ]); -# Utils::Api::check_interval_history('negative fees - callee',$callee->{customer}->{id},[ -# { cash => $balance - $callee_costs, -# profile => $provider->{subscriber_fees}->[0]->{profile}->{id} }, -# ]); -# } -# -#} - -done_testing(); -exit; - -sub create_provider { - my $rate_interval = shift; - $rate_interval //= 60; - return Utils::Api::setup_provider('test.com', - [ #rates: - { #any - onpeak_init_rate => 2, - onpeak_init_interval => $rate_interval, - onpeak_follow_rate => 1, - onpeak_follow_interval => $rate_interval, - offpeak_init_rate => 2, - offpeak_init_interval => $rate_interval, - offpeak_follow_rate => 1, - offpeak_follow_interval => $rate_interval, - }, - ], - [ #billing networks: - ] - ); -}