You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
891 lines
36 KiB
891 lines
36 KiB
package NGCP::Panel::Utils::Contract;
|
|
use strict;
|
|
use warnings;
|
|
|
|
use Sipwise::Base;
|
|
use DBIx::Class::Exception;
|
|
use NGCP::Panel::Utils::DateTime;
|
|
use DateTime::Format::Strptime qw();
|
|
|
|
|
|
#sub get_contract_balance {
|
|
# my (%params) = @_;
|
|
# my $c = $params{c};
|
|
# my $contract = $params{contract};
|
|
# my $profile = $params{profile};
|
|
# my $stime = $params{stime};
|
|
# my $etime = $params{etime};
|
|
# my $schema = $params{schema} // $c->model('DB');
|
|
#
|
|
# my $balance = $contract->contract_balances
|
|
# ->find({
|
|
# start => { '>=' => $stime },
|
|
# end => { '<=' => $etime },
|
|
# });
|
|
# unless($balance) {
|
|
# $balance = create_contract_balance(
|
|
# c => $c,
|
|
# profile => $profile,
|
|
# contract => $contract,
|
|
# stime => $stime,
|
|
# etime => $etime,
|
|
# schema => $schema,
|
|
# );
|
|
# }
|
|
# return $balance;
|
|
#}
|
|
#
|
|
#sub create_contract_balance {
|
|
# my %params = @_;
|
|
#
|
|
# my $c = $params{c};
|
|
# my $contract = $params{contract};
|
|
# my $profile = $params{profile};
|
|
# my $schema = $params{schema} // $c->model('DB');
|
|
#
|
|
# # first, calculate start and end time of current billing profile
|
|
# # (we assume billing interval of 1 month)
|
|
# my $stime = $params{stime} || NGCP::Panel::Utils::DateTime::current_local->truncate(to => 'month');
|
|
# my $etime = $params{etime} || $stime->clone->add(months => 1)->subtract(seconds => 1);
|
|
#
|
|
# # calculate free_time/cash ratio
|
|
# my ($cash_balance, $cash_balance_interval,
|
|
# $free_time_balance, $free_time_balance_interval) = get_contract_balance_values(
|
|
# interval_free_time => ( $profile->interval_free_time || 0 ),
|
|
# interval_free_cash => ( $profile->interval_free_cash || 0 ),
|
|
# stime => $stime,
|
|
# etime => $etime,
|
|
# );
|
|
#
|
|
# my $balance;
|
|
# try {
|
|
# $schema->txn_do(sub {
|
|
# $balance = $schema->resultset('contract_balances')->create({
|
|
# contract_id => $contract->id,
|
|
# cash_balance => $cash_balance,
|
|
# cash_balance_interval => $cash_balance_interval,
|
|
# free_time_balance => $free_time_balance,
|
|
# free_time_balance_interval => $free_time_balance_interval,
|
|
# start => $stime,
|
|
# end => $etime,
|
|
# });
|
|
# });
|
|
# } catch($e) {
|
|
# if ($e =~ /Duplicate entry/) {
|
|
# $c->log->warn("Creating contract balance failed: Duplicate entry. Ignoring!");
|
|
# } else {
|
|
# $c->log->error("Creating contract balance failed: " . $e);
|
|
# $e->rethrow;
|
|
# }
|
|
# };
|
|
# return $balance;
|
|
#}
|
|
#
|
|
#sub get_contract_balance_values {
|
|
# my %params = @_;
|
|
# my($free_time, $free_cash, $stime, $etime) = @params{qw/interval_free_time interval_free_cash stime etime/};
|
|
# my ($cash_balance, $cash_balance_interval,
|
|
# $free_time_balance, $free_time_balance_interval) = (0,0,0,0);
|
|
# if($free_time or $free_cash) {
|
|
# $etime->add(seconds => 1);
|
|
# my $ctime = NGCP::Panel::Utils::DateTime::current_local->truncate(to => 'day');
|
|
# if( ( $ctime->epoch >= $stime->epoch ) && ( $ctime->epoch <= $etime->epoch ) ){
|
|
# my $ratio = ($etime->epoch - $ctime->epoch) / ($etime->epoch - $stime->epoch);
|
|
#
|
|
# $cash_balance = sprintf("%.4f", $free_cash * $ratio);
|
|
# $cash_balance_interval = 0;
|
|
#
|
|
# $free_time_balance = sprintf("%.0f", $free_time * $ratio);
|
|
# $free_time_balance_interval = 0;
|
|
# }
|
|
# $etime->subtract(seconds => 1);
|
|
# }
|
|
# return ($cash_balance, $cash_balance_interval, $free_time_balance, $free_time_balance_interval);
|
|
#}
|
|
|
|
sub recursively_lock_contract {
|
|
my %params = @_;
|
|
|
|
my $c = $params{c};
|
|
my $contract = $params{contract};
|
|
my $schema = $params{schema} // $c->model('DB');
|
|
my $status = $contract->status;
|
|
if($status eq 'terminated') {
|
|
$contract->autoprov_field_devices->delete_all;
|
|
}
|
|
|
|
# first, change all voip subscribers, in case there are any
|
|
# we dont need to set to active, or any other level, already terminated subscribers
|
|
for my $subscriber($contract->voip_subscribers->search_rs({ 'me.status' => { '!=' => 'terminated' } })->all) {
|
|
$subscriber->update({ status => $status });
|
|
if($status eq 'terminated') {
|
|
NGCP::Panel::Utils::Subscriber::terminate(
|
|
c => $c, subscriber => $subscriber,
|
|
);
|
|
} elsif($status eq 'locked') {
|
|
NGCP::Panel::Utils::Subscriber::lock_provisoning_voip_subscriber(
|
|
c => $c,
|
|
prov_subscriber => $subscriber->provisioning_voip_subscriber,
|
|
level => 4,
|
|
) if($subscriber->provisioning_voip_subscriber);
|
|
} elsif($status eq 'active') {
|
|
NGCP::Panel::Utils::Subscriber::lock_provisoning_voip_subscriber(
|
|
c => $c,
|
|
prov_subscriber => $subscriber->provisioning_voip_subscriber,
|
|
level => 0,
|
|
) if($subscriber->provisioning_voip_subscriber);
|
|
}
|
|
}
|
|
|
|
# then, check all child contracts in case of reseller
|
|
my $resellers = $schema->resultset('resellers')->search({
|
|
contract_id => $contract->id,
|
|
});
|
|
for my $reseller($resellers->all) {
|
|
|
|
if($status eq 'terminated') {
|
|
# remove domains in case of reseller termination
|
|
for my $domain($reseller->domain_resellers->all) {
|
|
my $prov_domain = $domain->domain->provisioning_voip_domain;
|
|
if ($prov_domain) {
|
|
$prov_domain->voip_dbaliases->delete;
|
|
$prov_domain->voip_dom_preferences->delete;
|
|
$prov_domain->provisioning_voip_subscribers->delete;
|
|
$prov_domain->delete;
|
|
}
|
|
$domain->domain->delete;
|
|
$domain->delete;
|
|
}
|
|
|
|
# remove admin logins in case of reseller termination
|
|
for my $admin($reseller->admins->all) {
|
|
if($admin->id == $c->user->id) {
|
|
die "Cannot delete the currently used account";
|
|
}
|
|
$admin->delete;
|
|
}
|
|
}
|
|
|
|
# fetch sub-contracts of this contract
|
|
my $customers = $c->model('DB')->resultset('contracts')->search({
|
|
'contact.reseller_id' => $reseller->id,
|
|
}, {
|
|
join => 'contact',
|
|
});
|
|
my $data = { status => $status };
|
|
$data->{terminate_timestamp} = NGCP::Panel::Utils::DateTime::current_local
|
|
if($status eq 'terminated');
|
|
for my $customer($customers->all) {
|
|
$customer->update($data);
|
|
for my $subscriber($customer->voip_subscribers->all) {
|
|
$subscriber->update({ status => $status });
|
|
if($status eq 'terminated') {
|
|
NGCP::Panel::Utils::Subscriber::terminate(
|
|
c => $c, subscriber => $subscriber,
|
|
);
|
|
} elsif($status eq 'locked') {
|
|
NGCP::Panel::Utils::Subscriber::lock_provisoning_voip_subscriber(
|
|
c => $c,
|
|
prov_subscriber => $subscriber->provisioning_voip_subscriber,
|
|
level => 4,
|
|
) if($subscriber->provisioning_voip_subscriber);
|
|
} elsif($status eq 'active') {
|
|
NGCP::Panel::Utils::Subscriber::lock_provisoning_voip_subscriber(
|
|
c => $c,
|
|
prov_subscriber => $subscriber->provisioning_voip_subscriber,
|
|
level => 0,
|
|
) if($subscriber->provisioning_voip_subscriber);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
sub get_contract_rs {
|
|
my %params = @_;
|
|
my ($schema,$now) = @params{qw/schema now/};
|
|
$now //= NGCP::Panel::Utils::DateTime::current_local;
|
|
my $dtf = $schema->storage->datetime_parser;
|
|
my $rs = $schema->resultset('contracts')
|
|
->search({
|
|
$params{include_terminated} ? () : ('me.status' => { '!=' => 'terminated' }),
|
|
},{
|
|
bind => [ ( $dtf->format_datetime($now) ) x 2],
|
|
'join' => { 'billing_mappings_actual' => { 'billing_mappings' => 'product'}},
|
|
'+select' => [
|
|
'billing_mappings.id',
|
|
'billing_mappings.start_date',
|
|
'billing_mappings.product_id',
|
|
],
|
|
'+as' => [
|
|
'billing_mapping_id',
|
|
'billing_mapping_start_date',
|
|
'product_id',
|
|
],
|
|
alias => 'me',
|
|
});
|
|
|
|
return $rs;
|
|
}
|
|
|
|
sub get_customer_rs {
|
|
my %params = @_;
|
|
my ($c,$now) = @params{qw/c now/};
|
|
|
|
my $customers = get_contract_rs(
|
|
schema => $c->model('DB'),
|
|
include_terminated => $params{include_terminated},
|
|
);
|
|
|
|
$customers = $customers->search({
|
|
'contact.reseller_id' => { '-not' => undef },
|
|
},{
|
|
join => 'contact',
|
|
});
|
|
|
|
if($c->user->roles eq "admin") {
|
|
} elsif($c->user->roles eq "reseller") {
|
|
$customers = $customers->search({
|
|
'contact.reseller_id' => $c->user->reseller_id,
|
|
});
|
|
} elsif($c->user->roles eq "subscriberadmin") {
|
|
$customers = $customers->search({
|
|
'contact.reseller_id' => $c->user->contract->contact->reseller_id,
|
|
});
|
|
}
|
|
|
|
$customers = $customers->search({
|
|
'-or' => [
|
|
'product.class' => 'sipaccount',
|
|
'product.class' => 'pbxaccount',
|
|
],
|
|
},{
|
|
'+select' => 'billing_mappings.id',
|
|
'+as' => 'bmid',
|
|
});
|
|
|
|
return $customers;
|
|
}
|
|
|
|
sub get_contract_zonesfees_rs {
|
|
my %params = @_;
|
|
my $c = $params{c};
|
|
my $stime = $params{stime};
|
|
my $etime = $params{etime};
|
|
my $contract_id = $params{contract_id};
|
|
my $subscriber_uuid = $params{subscriber_uuid};
|
|
my $group_detail = $params{group_by_detail};
|
|
|
|
my $zonecalls_rs_out = $c->model('DB')->resultset('cdr')->search( {
|
|
'call_status' => 'ok',
|
|
'source_user_id' => ($subscriber_uuid || { '!=' => '0' }),
|
|
start_time =>
|
|
[ -and =>
|
|
{ '>=' => $stime->epoch},
|
|
{ '<=' => $etime->epoch},
|
|
],
|
|
source_account_id => $contract_id,
|
|
},{
|
|
'select' => [
|
|
{ sum => 'me.source_customer_cost', -as => 'customercost' },
|
|
{ sum => 'me.source_carrier_cost', -as => 'carriercost' },
|
|
{ sum => 'me.source_reseller_cost', -as => 'resellercost' },
|
|
{ sum => 'me.source_customer_free_time', -as => 'free_time' },
|
|
{ sum => 'me.duration', -as => 'duration' },
|
|
{ count => '*', -as => 'number' },
|
|
'source_customer_billing_zones_history.zone',
|
|
$group_detail ? 'source_customer_billing_zones_history.detail' : (),
|
|
],
|
|
'as' => [
|
|
qw/customercost carriercost resellercost free_time duration number zone/,
|
|
$group_detail ? 'zone_detail' : (),
|
|
],
|
|
join => 'source_customer_billing_zones_history',
|
|
group_by => [
|
|
'source_customer_billing_zones_history.zone',
|
|
$group_detail ? 'source_customer_billing_zones_history.detail' : (),
|
|
],
|
|
order_by => 'source_customer_billing_zones_history.zone',
|
|
} );
|
|
|
|
my $zonecalls_rs_in = $c->model('DB')->resultset('cdr')->search( {
|
|
'call_status' => 'ok',
|
|
'destination_user_id' => ($subscriber_uuid || { '!=' => '0' }),
|
|
start_time =>
|
|
[ -and =>
|
|
{ '>=' => $stime->epoch},
|
|
{ '<=' => $etime->epoch},
|
|
],
|
|
destination_account_id => $contract_id,
|
|
},{
|
|
'select' => [
|
|
{ sum => 'me.destination_customer_cost', -as => 'customercost' },
|
|
{ sum => 'me.destination_carrier_cost', -as => 'carriercost' },
|
|
{ sum => 'me.destination_reseller_cost', -as => 'resellercost' },
|
|
{ sum => 'me.destination_customer_free_time', -as => 'free_time' },
|
|
{ sum => 'me.duration', -as => 'duration' },
|
|
{ count => '*', -as => 'number' },
|
|
'destination_customer_billing_zones_history.zone',
|
|
$group_detail ? 'destination_customer_billing_zones_history.detail' : (),
|
|
],
|
|
'as' => [
|
|
qw/customercost carriercost resellercost free_time duration number zone/,
|
|
$group_detail ? 'zone_detail' : (),
|
|
],
|
|
join => 'destination_customer_billing_zones_history',
|
|
group_by => [
|
|
'destination_customer_billing_zones_history.zone',
|
|
$group_detail ? 'destination_customer_billing_zones_history.detail' : (),
|
|
],
|
|
order_by => 'destination_customer_billing_zones_history.zone',
|
|
} );
|
|
|
|
return ($zonecalls_rs_in, $zonecalls_rs_out);
|
|
}
|
|
|
|
sub get_contract_zonesfees {
|
|
my %params = @_;
|
|
|
|
my $c = $params{c};
|
|
my $in = delete $params{in};
|
|
my $out = delete $params{out};
|
|
|
|
my ($zonecalls_rs_in, $zonecalls_rs_out) = get_contract_zonesfees_rs(%params);
|
|
my @zones = (
|
|
$in ? $zonecalls_rs_in->all : (),
|
|
$out ? $zonecalls_rs_out->all : (),
|
|
);
|
|
|
|
my %allzones;
|
|
for my $zone (@zones) {
|
|
my $zname = $params{group_by_detail} ?
|
|
($zone->get_column('zone')//'') . ' ' . ($zone->get_column('zone_detail')//'') :
|
|
($zone->get_column('zone')//'');
|
|
|
|
my %cols = $zone->get_inflated_columns;
|
|
if($c->user->roles eq "admin") {
|
|
$allzones{$zname}{carriercost} += $cols{carriercost} || 0;
|
|
}
|
|
if($c->user->roles eq "admin" || $c->user->roles eq "reseller") {
|
|
$allzones{$zname}{resellercost} += $cols{resellercost} || 0;
|
|
}
|
|
$allzones{$zname}{customercost} += $cols{customercost} || 0;
|
|
$allzones{$zname}{duration} += $cols{duration} || 0;
|
|
$allzones{$zname}{free_time} += $cols{free_time} || 0;
|
|
$allzones{$zname}{number} += $cols{number} || 0;
|
|
$allzones{$zname}{zone} = $zone->get_column('zone')//'';
|
|
$allzones{$zname}{zone_detail} = $zone->get_column('zone_detail')//'';
|
|
}
|
|
|
|
return \%allzones;
|
|
}
|
|
|
|
sub get_contract_calls_rs{
|
|
my %params = @_;
|
|
(my($c,$customer_contract_id,$stime,$etime)) = @params{qw/c customer_contract_id stime etime/};
|
|
|
|
$stime ||= NGCP::Panel::Utils::DateTime::current_local()->truncate( to => 'month' );
|
|
$etime ||= $stime->clone->add( months => 1 );
|
|
|
|
my $calls_rs = $c->model('DB')->resultset('cdr')->search( {
|
|
# source_user_id => { 'in' => [ map {$_->uuid} @{$contract->{subscriber}} ] },
|
|
'call_status' => 'ok',
|
|
'source_user_id' => { '!=' => '0' },
|
|
'start_time' =>
|
|
[ -and =>
|
|
{ '>=' => $stime->epoch},
|
|
{ '<=' => $etime->epoch},
|
|
],
|
|
'source_account_id' => $customer_contract_id,
|
|
},{
|
|
select => [qw/
|
|
source_user source_domain source_cli
|
|
destination_user_in
|
|
start_time duration call_type
|
|
source_customer_cost
|
|
source_customer_billing_zones_history.zone
|
|
source_customer_billing_zones_history.detail
|
|
/],
|
|
as => [qw/
|
|
source_user source_domain source_cli
|
|
destination_user_in
|
|
start_time duration call_type
|
|
source_customer_cost
|
|
zone
|
|
zone_detail
|
|
/],
|
|
'join' => 'source_customer_billing_zones_history',
|
|
'order_by' => 'start_time',
|
|
} );
|
|
|
|
return $calls_rs;
|
|
}
|
|
|
|
sub prepare_billing_mappings {
|
|
my (%params) = @_;
|
|
|
|
my ($c,$resource,$old_resource,$mappings_to_create,$now,$delete_mappings,$err_code,$billing_profile_field,$billing_profiles_field,$profile_package_field,$billing_profile_definition_field) = @params{qw/c resource old_resource mappings_to_create now delete_mappings err_code billing_profile_field billing_profiles_field product_package_field billing_profile_definition_field/};
|
|
|
|
my $schema = $c->model('DB');
|
|
if (!defined $err_code || ref $err_code ne 'CODE') {
|
|
$err_code = sub { return 0; };
|
|
}
|
|
|
|
my $profile_def_mode = $resource->{billing_profile_definition} // 'id';
|
|
$now //= NGCP::Panel::Utils::DateTime::current_local;
|
|
|
|
my $reseller_id = undef;
|
|
my $is_customer = 1;
|
|
if (defined $resource->{contact_id}) {
|
|
my $contact = $schema->resultset('contacts')->find($resource->{contact_id});
|
|
if ($contact) {
|
|
$reseller_id = $contact->reseller_id; #($contact->reseller_id // -1);
|
|
$is_customer = defined $reseller_id;
|
|
}
|
|
}
|
|
|
|
my $product_id = undef; #any subsequent create will fail without product_id
|
|
my $prepaid = undef;
|
|
my $billing_profile_id = undef;
|
|
if (defined $old_resource) {
|
|
# TODO: what about changed product, do we allow it?
|
|
my $billing_mapping = $schema->resultset('billing_mappings')->find($old_resource->{billing_mapping_id});
|
|
$product_id = $billing_mapping->product->id;
|
|
$prepaid = $billing_mapping->billing_profile->prepaid;
|
|
$billing_profile_id = $billing_mapping->billing_profile->id;
|
|
} else {
|
|
if (exists $resource->{type} || exists $c->stash->{type}) {
|
|
my $productclass = (exists $c->stash->{type} ? $c->stash->{type} : $resource->{type});
|
|
my $product = $schema->resultset('products')->find({ class => $productclass });
|
|
if ($product) {
|
|
$product_id = $product->id;
|
|
}
|
|
} elsif (exists $resource->{product_id}) {
|
|
$product_id = $resource->{product_id};
|
|
}
|
|
}
|
|
|
|
if ('id' eq $profile_def_mode) {
|
|
my $delete = undef;
|
|
if (defined $old_resource) { #update
|
|
if (defined $resource->{billing_profile_id}) {
|
|
if ($billing_profile_id != $resource->{billing_profile_id}) {
|
|
#change profile:
|
|
$delete = 0; #1; #delete future mappings?
|
|
my $entities = {};
|
|
return 0 unless _check_profile_network(c => $c, reseller_id => $reseller_id, err_code => $err_code, entities => $entities,
|
|
resource => $resource,
|
|
profile_id_field => 'billing_profile_id',
|
|
field => $billing_profile_field,
|
|
);
|
|
my ($profile) = @$entities{qw/profile/};
|
|
push(@$mappings_to_create,{billing_profile_id => $profile->id,
|
|
network_id => undef,
|
|
product_id => $product_id,
|
|
start_date => $now,
|
|
end_date => undef,
|
|
});
|
|
} else {
|
|
#not changed, don't touch mappings
|
|
$delete = 0;
|
|
}
|
|
} else {
|
|
#undef profile is not allowed
|
|
$delete = 0;
|
|
my $entities = {};
|
|
return 0 unless _check_profile_network(c => $c, reseller_id => $reseller_id, err_code => $err_code, entities => $entities,
|
|
resource => $resource,
|
|
profile_id_field => 'billing_profile_id',
|
|
field => $billing_profile_field,
|
|
);
|
|
}
|
|
} else { #create
|
|
$delete = 1; #for the sake of completeness
|
|
my $entities = {};
|
|
return 0 unless _check_profile_network(c => $c, reseller_id => $reseller_id, err_code => $err_code, entities => $entities,
|
|
resource => $resource,
|
|
profile_id_field => 'billing_profile_id',
|
|
field => $billing_profile_field,
|
|
);
|
|
my ($profile) = @$entities{qw/profile/};
|
|
push(@$mappings_to_create,{billing_profile_id => $profile->id,
|
|
network_id => undef,
|
|
product_id => $product_id,
|
|
#we don't change the former behaviour in update situations:
|
|
start_date => undef,
|
|
end_date => undef,
|
|
});
|
|
}
|
|
if (defined $delete_mappings && ref $delete_mappings eq 'SCALAR') {
|
|
$$delete_mappings = $delete;
|
|
}
|
|
delete $resource->{profile_package_id};
|
|
} elsif ('profiles' eq $profile_def_mode) {
|
|
if (!defined $resource->{billing_profiles}) {
|
|
$resource->{billing_profiles} //= [];
|
|
}
|
|
if (ref $resource->{billing_profiles} ne "ARRAY") {
|
|
return 0 unless &{$err_code}("Invalid field 'billing_profiles'. Must be an array.",$billing_profiles_field);
|
|
}
|
|
my %interval_type_counts = ( open => 0, open_any_network => 0, 'open end' => 0, 'open start' => 0, 'start-end' => 0 );
|
|
my $dtf = $schema->storage->datetime_parser;
|
|
foreach my $mapping (@{$resource->{billing_profiles}}) {
|
|
if (ref $mapping ne "HASH") {
|
|
return 0 unless &{$err_code}("Invalid element in array 'billing_profiles'. Must be an object.",$billing_profiles_field);
|
|
}
|
|
my $entities = {};
|
|
return 0 unless _check_profile_network(c => $c, reseller_id => $reseller_id, err_code => $err_code, entities => $entities,
|
|
resource => $mapping,
|
|
field => $billing_profiles_field,
|
|
profile_id_field => 'profile_id',
|
|
network_id_field => 'network_id',
|
|
);
|
|
my ($profile,$network) = @$entities{qw/profile network/};
|
|
if (defined $prepaid) {
|
|
if ($profile->prepaid != $prepaid) {
|
|
return 0 unless &{$err_code}("Switching between prepaid and post-paid billing profiles is not supported (" . $profile->name . ").",$billing_profiles_field);
|
|
}
|
|
} else {
|
|
$prepaid = $profile->prepaid;
|
|
}
|
|
|
|
# TODO: what about changed product, do we allow it?
|
|
#my $product_class = delete $mapping->{type};
|
|
#unless( (defined $product_class ) && ($product_class eq "sipaccount" || $product_class eq "pbxaccount") ) {
|
|
# return 0 unless &{$err_code}("Mandatory 'type' parameter is empty or invalid, must be 'sipaccount' or 'pbxaccount'.");
|
|
#}
|
|
#my $product = $schema->resultset('products')->find({ class => $product_class });
|
|
#unless($product) {
|
|
# return 0 unless &{$err_code}("Invalid 'type'.");
|
|
#} else {
|
|
# # add product_id just for form check (not part of the actual contract item)
|
|
# # and remove it after the check
|
|
# $mapping->{product_id} = $product->id;
|
|
#}
|
|
|
|
my $start = (defined $mapping->{start} ? NGCP::Panel::Utils::DateTime::from_string($mapping->{start}) : undef);
|
|
my $stop = (defined $mapping->{stop} ? NGCP::Panel::Utils::DateTime::from_string($mapping->{stop}) : undef);
|
|
|
|
if (!defined $start && !defined $stop) { #open interval
|
|
$interval_type_counts{open} += 1;
|
|
$interval_type_counts{open_any_network} += 1 unless $network;
|
|
} elsif (defined $start && !defined $stop) { #open end interval
|
|
my $start_str = $dtf->format_datetime($start);
|
|
if ($start <= $now) {
|
|
return 0 unless &{$err_code}("'start' timestamp ($start_str) is not in future.",$billing_profiles_field);
|
|
}
|
|
#if (exists $start_dupes{$start_str}) {
|
|
# $start_dupes{$start_str} += 1;
|
|
# return 0 unless &{$err_code}("Identical 'start' timestamps ($start_str) not allowed.");
|
|
#} else {
|
|
# $start_dupes{$start_str} = 1;
|
|
#}
|
|
$interval_type_counts{'open end'} += 1;
|
|
} elsif (!defined $start && defined $stop) { #open start interval
|
|
my $stop_str = $dtf->format_datetime($stop);
|
|
return 0 unless &{$err_code}("Interval with 'stop' timestamp ($stop_str) but no 'start' timestamp specified.",$billing_profiles_field);
|
|
$interval_type_counts{'open start'} //= 0;
|
|
$interval_type_counts{'open start'} += 1;
|
|
} else { #start-end interval
|
|
my $start_str = $dtf->format_datetime($start);
|
|
if ($start <= $now) {
|
|
return 0 unless &{$err_code}("'start' timestamp ($start_str) is not in future.",$billing_profiles_field);
|
|
}
|
|
my $stop_str = $dtf->format_datetime($stop);
|
|
if ($start >= $stop) {
|
|
return 0 unless &{$err_code}("'start' timestamp ($start_str) has to be before 'stop' timestamp ($stop_str).",$billing_profiles_field);
|
|
}
|
|
#if (exists $start_dupes{$start_str}) {
|
|
# $start_dupes{$start_str} += 1;
|
|
# return 0 unless &{$err_code}("Identical 'start' timestamps ($start_str) not allowed.");
|
|
#} else {
|
|
# $start_dupes{$start_str} = 1;
|
|
#}
|
|
$interval_type_counts{'start-end'} += 1;
|
|
}
|
|
|
|
push(@$mappings_to_create,{
|
|
billing_profile_id => $profile->id,
|
|
network_id => ($is_customer && defined $network ? $network->id : undef),
|
|
product_id => $product_id,
|
|
start_date => $start,
|
|
end_date => $stop,
|
|
});
|
|
}
|
|
|
|
if (!defined $old_resource && $interval_type_counts{'open_any_network'} < 1) {
|
|
return 0 unless &{$err_code}("An interval without 'start' and 'stop' timestamps and no billing network is required.",$billing_profiles_field);
|
|
} elsif (defined $old_resource && $interval_type_counts{'open'} > 0) {
|
|
return 0 unless &{$err_code}("Adding intervals without 'start' and 'stop' timestamps is not allowed.",$billing_profiles_field);
|
|
}
|
|
if (defined $delete_mappings && ref $delete_mappings eq 'SCALAR') {
|
|
$$delete_mappings = 1; #always clear future mappings to place new ones
|
|
}
|
|
delete $resource->{profile_package_id};
|
|
} elsif ('package' eq $profile_def_mode) {
|
|
if (!$is_customer) {
|
|
return 0 unless &{$err_code}("Setting a profile package is supported for customer contracts only.",$billing_profile_definition_field);
|
|
}
|
|
my $delete = undef;
|
|
if (defined $old_resource) { #update
|
|
if (defined $old_resource->{profile_package_id} && !defined $resource->{profile_package_id}) {
|
|
#clear package: don't touch billing mappings (just clear profile package)
|
|
$delete = 0;
|
|
} elsif (!defined $old_resource->{profile_package_id} && defined $resource->{profile_package_id}) {
|
|
#set package: apply initial mappings
|
|
$delete = 0; #1; #delete future mappings?
|
|
my $entities = {};
|
|
return 0 unless _check_profile_package(c => $c, reseller_id => $reseller_id, err_code => $err_code, entities => $entities,
|
|
package_id => $resource->{profile_package_id},
|
|
field => $profile_package_field,
|
|
);
|
|
my ($package) = @$entities{qw/package/};
|
|
foreach my $mapping ($package->initial_profiles->all) {
|
|
push(@$mappings_to_create,{ #assume not terminated,
|
|
billing_profile_id => $mapping->profile_id,
|
|
network_id => ($is_customer ? $mapping->network_id : undef),
|
|
product_id => $product_id,
|
|
start_date => $now,
|
|
end_date => undef,
|
|
});
|
|
}
|
|
} elsif (defined $old_resource->{profile_package_id} && defined $resource->{profile_package_id}) {
|
|
if ($old_resource->{profile_package_id} != $resource->{profile_package_id}) {
|
|
#change package: apply initial mappings
|
|
$delete = 0; #1; #delete future mappings?
|
|
my $entities = {};
|
|
return 0 unless _check_profile_package(c => $c, reseller_id => $reseller_id, err_code => $err_code, entities => $entities,
|
|
package_id => $resource->{profile_package_id},
|
|
field => $profile_package_field,
|
|
);
|
|
my ($package) = @$entities{qw/package/};
|
|
foreach my $mapping ($package->initial_profiles->all) {
|
|
push(@$mappings_to_create,{ #assume not terminated,
|
|
billing_profile_id => $mapping->profile_id,
|
|
network_id => ($is_customer ? $mapping->network_id : undef),
|
|
product_id => $product_id,
|
|
start_date => $now,
|
|
end_date => undef,
|
|
});
|
|
}
|
|
} else {
|
|
#package unchanged: don't touch billing mappings
|
|
$delete = 0;
|
|
}
|
|
} else {
|
|
#package unchanged (null): don't touch billing mappings
|
|
$delete = 0;
|
|
}
|
|
} else { #create
|
|
$delete = 1; #for the sake of completeness
|
|
my $entities = {};
|
|
return 0 unless _check_profile_package(c => $c, reseller_id => $reseller_id, err_code => $err_code, entities => $entities,
|
|
package_id => $resource->{profile_package_id},
|
|
field => $profile_package_field,
|
|
);
|
|
my ($package) = @$entities{qw/package/};
|
|
foreach my $mapping ($package->initial_profiles->all) {
|
|
push(@$mappings_to_create,{ #assume not terminated,
|
|
billing_profile_id => $mapping->profile_id,
|
|
network_id => ($is_customer ? $mapping->network_id : undef),
|
|
product_id => $product_id,
|
|
start_date => undef, #$now,
|
|
end_date => undef,
|
|
});
|
|
}
|
|
}
|
|
if (defined $delete_mappings && ref $delete_mappings eq 'SCALAR') {
|
|
$$delete_mappings = $delete;
|
|
}
|
|
} else {
|
|
return 0 unless &{$err_code}("Invalid 'billing_profile_definition'.",$billing_profile_definition_field);
|
|
}
|
|
|
|
delete $resource->{billing_profile_id};
|
|
delete $resource->{billing_profiles};
|
|
|
|
delete $resource->{billing_profile_definition};
|
|
|
|
return 1;
|
|
}
|
|
|
|
sub _check_profile_network {
|
|
my (%params) = @_;
|
|
my ($c,$res,$profile_id_field,$network_id_field,$field,$reseller_id,$err_code,$entities) = @params{qw/c resource profile_id_field network_id_field field reseller_id err_code entities/};
|
|
|
|
my $schema = $c->model('DB');
|
|
if (!defined $err_code || ref $err_code ne 'CODE') {
|
|
$err_code = sub { return 0; };
|
|
}
|
|
|
|
unless(defined $res->{$profile_id_field}) {
|
|
return 0 unless &{$err_code}("Invalid '$profile_id_field', not defined.",$field);
|
|
}
|
|
my $profile = $schema->resultset('billing_profiles')->find($res->{$profile_id_field});
|
|
unless($profile) {
|
|
return 0 unless &{$err_code}("Invalid '$profile_id_field' ($res->{$profile_id_field}).",$field);
|
|
}
|
|
if ($profile->status eq 'terminated') {
|
|
return 0 unless &{$err_code}("Invalid '$profile_id_field' ($res->{$profile_id_field}), already terminated.",$field);
|
|
}
|
|
if (defined $reseller_id && defined $profile->reseller_id && $reseller_id != $profile->reseller_id) { #($profile->reseller_id // -1)) {
|
|
return 0 unless &{$err_code}("The reseller of the contact doesn't match the reseller of the billing profile (" . $profile->name . ").",$field);
|
|
}
|
|
my $network;
|
|
if (defined $network_id_field && defined $res->{$network_id_field}) {
|
|
$network = $schema->resultset('billing_networks')->find($res->{$network_id_field});
|
|
unless($network) {
|
|
return 0 unless &{$err_code}("Invalid '$network_id_field' ($res->{$network_id_field}).",$field);
|
|
}
|
|
if (defined $reseller_id && defined $network->reseller_id && $reseller_id != $network->reseller_id) { #($network->reseller_id // -1)) {
|
|
return 0 unless &{$err_code}("The reseller of the contact doesn't match the reseller of the billing network (" . $network->name . ").",$field);
|
|
}
|
|
}
|
|
if (defined $entities and ref $entities eq 'HASH') {
|
|
$entities->{profile} = $profile;
|
|
$entities->{network} = $network;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
sub _check_profile_package {
|
|
my (%params) = @_;
|
|
my ($c,$res,$package_id,$reseller_id,$field,$err_code,$entities) = @params{qw/c resource package_id reseller_id field err_code entities/};
|
|
|
|
my $schema = $c->model('DB');
|
|
if (!defined $err_code || ref $err_code ne 'CODE') {
|
|
$err_code = sub { return 0; };
|
|
}
|
|
|
|
unless(defined $package_id) {
|
|
return 0 unless &{$err_code}("Invalid 'profile_package_id', not defined.",$field);
|
|
}
|
|
my $package = $schema->resultset('profile_packages')->find($package_id);
|
|
unless($package) {
|
|
return 0 unless &{$err_code}("Invalid 'profile_package_id'.",$field);
|
|
}
|
|
if ($package->status eq 'terminated') {
|
|
return 0 unless &{$err_code}("Invalid 'profile_package_id', already terminated.",$field);
|
|
}
|
|
if (defined $reseller_id && defined $package->reseller_id && $reseller_id != $package->reseller_id) {
|
|
return 0 unless &{$err_code}("The reseller of the contact doesn't match the reseller of the profile package (" . $package->name . ").",$field);
|
|
}
|
|
|
|
if (defined $entities and ref $entities eq 'HASH') {
|
|
$entities->{package} = $package;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
sub resource_from_future_mappings {
|
|
my ($contract) = @_;
|
|
return resource_from_mappings($contract,1);
|
|
}
|
|
|
|
sub resource_from_mappings {
|
|
|
|
my ($contract,$future_only) = @_;
|
|
|
|
my $is_customer = (defined $contract->contact->reseller_id ? 1 : 0);
|
|
my @mappings_resource = ();
|
|
|
|
my $datetime_fmt = DateTime::Format::Strptime->new(
|
|
pattern => '%F %T',
|
|
); #validate_forms uses RFC3339 otherwise, which contains the tz offset part
|
|
|
|
foreach my $mapping (billing_mappings_ordered($future_only ? future_billing_mappings($contract->billing_mappings) : $contract->billing_mappings)->all) {
|
|
my %m = $mapping->get_inflated_columns;
|
|
delete $m{id};
|
|
$m{start} = delete $m{start_date};
|
|
$m{stop} = delete $m{end_date};
|
|
$m{start} = $datetime_fmt->format_datetime($m{start}) if defined $m{start};
|
|
$m{stop} = $datetime_fmt->format_datetime($m{stop}) if defined $m{stop};
|
|
$m{profile_id} = delete $m{billing_profile_id};
|
|
delete $m{contract_id};
|
|
delete $m{product_id};
|
|
delete $m{network_id} unless $is_customer;
|
|
push(@mappings_resource,\%m);
|
|
}
|
|
|
|
return \@mappings_resource;
|
|
|
|
}
|
|
|
|
sub billing_mappings_ordered {
|
|
my ($rs,$now,$actual_bmid) = @_;
|
|
|
|
my $dtf;
|
|
$dtf = $rs->result_source->schema->storage->datetime_parser if defined $now;
|
|
|
|
my @select = ();
|
|
if ($now) {
|
|
push(@select,{ '' => \[ 'if(`me`.`start_date` is null,0,`me`.`start_date` > ?)', $dtf->format_datetime($now) ], -as => 'is_future' });
|
|
}
|
|
if ($actual_bmid) {
|
|
push(@select,{ '' => \[ '`me`.`id` = ?', $actual_bmid ], -as => 'is_actual' });
|
|
}
|
|
|
|
return $rs->search_rs(
|
|
{},
|
|
{ order_by => { '-asc' => ['start_date', 'id']},
|
|
(scalar @select == 1 ? ('+select' => $select[0]) : ()),
|
|
(scalar @select > 1 ? ('+select' => \@select) : ()),
|
|
});
|
|
|
|
}
|
|
|
|
sub remove_future_billing_mappings {
|
|
|
|
my ($contract,$now) = @_;
|
|
$now //= NGCP::Panel::Utils::DateTime::current_local;
|
|
|
|
future_billing_mappings($contract->billing_mappings,$now)->delete;
|
|
|
|
}
|
|
|
|
sub future_billing_mappings {
|
|
|
|
my ($rs,$now) = @_;
|
|
$now //= NGCP::Panel::Utils::DateTime::current_local;
|
|
|
|
return $rs->search_rs({start_date => { '>' => $now },});
|
|
|
|
}
|
|
|
|
1;
|
|
|
|
__END__
|
|
|
|
=head1 NAME
|
|
|
|
NGCP::Panel::Utils::Contract
|
|
|
|
=head1 DESCRIPTION
|
|
|
|
A temporary helper to manipulate Contract related data
|
|
|
|
=head1 METHODS
|
|
|
|
=head2 create_contract_balance
|
|
|
|
Parameters:
|
|
c The controller
|
|
contract The contract resultset
|
|
profile The billing_profile resultset
|
|
|
|
Creates a contract balance for the current month, if none exists yet
|
|
for this contract.
|
|
|
|
=head1 AUTHOR
|
|
|
|
Andreas Granig,
|
|
|
|
=head1 LICENSE
|
|
|
|
This library is free software. You can redistribute it and/or modify
|
|
it under the same terms as Perl itself.
|
|
|
|
=cut
|
|
# vim: set tabstop=4 expandtab:
|