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.
ngcp-panel/lib/NGCP/Panel/Utils/Contract.pm

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: