MT#13903 balance interval catchup and resize WIP

+ synchronized contract balance catchup
+ balance interval resizing upon profile package transitions
+ dynamic interval length, interval start, carry-over propagation, ..
+ end-of-month 'preserve' mode correction for strictly aligned month intervals for start_mode=customer "create" timestamp
+ new api/balanceintervals resource to inspect contracts' balance interval histories
+ updated affected panel UI controllers
+ test case with time warps using Time::Fake

caveats:
- creating an invoice for a given 'period' (month) has to be refactored to select a disitnct balance interval. right now it takes the last interval in the month specified.
- generate_invoice.pl is broken and needs a major refactoring

Change-Id: I7bb54a83b76e510b1baa573a986d05400a7fec1e
changes/73/2073/5
Rene Krenn 10 years ago
parent 0316e08a07
commit c378681a24

1
debian/control vendored

@ -95,6 +95,7 @@ Depends: gettext,
libdatetime-format-mysql-perl,
libdatetime-format-rfc3339-perl,
libdatetime-perl,
libtime-fake-perl,
libdbix-class-resultset-recursiveupdate-perl (>= 0.30~),
libemail-mime-perl,
libemail-sender-perl,

@ -1,6 +1,6 @@
package NGCP::Panel;
use Moose;
use Moose;
use Catalyst::Runtime 5.80;
# Set flags and add plugins for the application.

@ -0,0 +1,189 @@
package NGCP::Panel::Controller::API::BalanceIntervals;
use Sipwise::Base;
use namespace::sweep;
use boolean qw(true);
use Data::HAL qw();
use Data::HAL::Link qw();
use HTTP::Headers qw();
use HTTP::Status qw(:constants);
use MooseX::ClassAttribute qw(class_has);
use NGCP::Panel::Utils::ProfilePackages qw();
use NGCP::Panel::Utils::DateTime;
use Path::Tiny qw(path);
use Safe::Isa qw($_isa);
BEGIN { extends 'Catalyst::Controller::ActionRole'; }
require Catalyst::ActionRole::ACL;
require Catalyst::ActionRole::CheckTrailingSlash;
require Catalyst::ActionRole::HTTPMethods;
require Catalyst::ActionRole::RequireSSL;
class_has 'api_description' => (
is => 'ro',
isa => 'Str',
default =>
'Histories of contracts\' cash balance intervals.',
);
class_has 'query_params' => (
is => 'ro',
isa => 'ArrayRef',
default => sub {[
{
param => 'reseller_id',
description => 'Filter for actual balance intervals of customers belonging to a specific reseller',
query => {
first => sub {
my $q = shift;
{ 'contact.reseller_id' => $q };
},
second => sub {
{ join => 'contact' };
},
},
},
{
param => 'contact_id',
description => 'Filter for contracts with a specific contact id',
query => {
first => sub {
my $q = shift;
{ contact_id => $q };
},
second => sub {},
},
},
{
param => 'status',
description => 'Filter for contracts with a specific status (except "terminated")',
query => {
first => sub {
my $q = shift;
{ status => $q };
},
second => sub {},
},
},
{
param => 'external_id',
description => 'Filter for contracts with a specific external id',
query => {
first => sub {
my $q = shift;
{ 'me.external_id' => { like => $q } };
},
second => sub {},
},
},
]},
);
with 'NGCP::Panel::Role::API::BalanceIntervals';
class_has('resource_name', is => 'ro', default => 'balanceintervals');
class_has('dispatch_path', is => 'ro', default => '/api/balanceintervals/');
class_has('relation', is => 'ro', default => 'http://purl.org/sipwise/ngcp-api/#rel-balanceintervals');
__PACKAGE__->config(
action => {
map { $_ => {
ACLDetachTo => '/api/root/invalid_user',
AllowedRole => [qw/admin reseller/],
Args => 0,
Does => [qw(ACL CheckTrailingSlash RequireSSL)],
Method => $_,
Path => __PACKAGE__->dispatch_path,
} } @{ __PACKAGE__->allowed_methods }
},
action_roles => [qw(HTTPMethods)],
);
sub auto :Private {
my ($self, $c) = @_;
$self->set_body($c);
$self->log_request($c);
#$self->apply_fake_time($c);
}
sub GET :Allow {
my ($self, $c) = @_;
my $page = $c->request->params->{page} // 1;
my $rows = $c->request->params->{rows} // 10;
$c->model('DB')->set_transaction_isolation('READ COMMITTED');
my $guard = $c->model('DB')->txn_scope_guard;
{
my $contracts = $self->item_rs($c)->search_rs(undef,{
for => 'update',
});
(my $total_count, $contracts) = $self->paginate_order_collection($c, $contracts);
my $now = NGCP::Panel::Utils::DateTime::current_local;
my (@embedded, @links);
my $form = $self->get_form($c);
for my $contract ($contracts->all) {
my $balance = NGCP::Panel::Utils::ProfilePackages::get_contract_balance(c => $c,
contract => $contract,
now => $now);
#sleep(5);
my $hal = $self->hal_from_balance($c, $balance, $form, 0); #we prefer item collection links pointing to the contract's collection instead of this root collection
$hal->_forcearray(1);
push @embedded, $hal;
my $link = Data::HAL::Link->new(relation => 'ngcp:'.$self->resource_name, href => sprintf('/%s%d/%d', $c->request->path, $contract->id, $balance->id));
$link->_forcearray(1);
push @links, $link;
#push @links, Data::HAL::Link->new(relation => 'collection', href => sprintf("/api/%s/%d/", $self->resource_name, $contract->id));
}
$self->delay_commit($c,$guard);
push @links,
Data::HAL::Link->new(
relation => 'curies',
href => 'http://purl.org/sipwise/ngcp-api/#rel-{rel}',
name => 'ngcp',
templated => true,
),
Data::HAL::Link->new(relation => 'profile', href => 'http://purl.org/sipwise/ngcp-api/');
push @links, $self->collection_nav_links($page, $rows, $total_count, $c->request->path, $c->request->query_params);
my $hal = Data::HAL->new(
embedded => [@embedded],
links => [@links],
);
$hal->resource({
total_count => $total_count,
});
my $response = HTTP::Response->new(HTTP_OK, undef,
HTTP::Headers->new($hal->http_headers(skip_links => 1)), $hal->as_json);
$c->response->headers($response->headers);
$c->response->body($response->content);
return;
}
return;
}
sub HEAD :Allow {
my ($self, $c) = @_;
$c->forward(qw(GET));
$c->response->body(q());
return;
}
sub OPTIONS :Allow {
my ($self, $c) = @_;
my $allowed_methods = $self->allowed_methods_filtered($c);
$c->response->headers(HTTP::Headers->new(
Allow => $allowed_methods->join(', '),
Accept_Post => 'application/hal+json; profile=http://purl.org/sipwise/ngcp-api/#rel-'.$self->resource_name,
));
$c->response->content_type('application/json');
$c->response->body(JSON::to_json({ methods => $allowed_methods })."\n");
return;
}
sub end : Private {
my ($self, $c) = @_;
#$self->reset_fake_time($c);
$self->log_response($c);
}
# vim: set tabstop=4 expandtab:

@ -0,0 +1,256 @@
package NGCP::Panel::Controller::API::BalanceIntervalsItem;
use Sipwise::Base;
use namespace::sweep;
use boolean qw(true);
use HTTP::Headers qw();
use HTTP::Status qw(:constants);
use MooseX::ClassAttribute qw(class_has);
use NGCP::Panel::Utils::DateTime;
use NGCP::Panel::Utils::ValidateJSON qw();
use Path::Tiny qw(path);
use Safe::Isa qw($_isa);
BEGIN { extends 'Catalyst::Controller::ActionRole'; }
require Catalyst::ActionRole::ACL;
require Catalyst::ActionRole::HTTPMethods;
require Catalyst::ActionRole::RequireSSL;
with 'NGCP::Panel::Role::API::BalanceIntervals';
class_has('resource_name', is => 'ro', default => 'balanceintervals');
class_has('dispatch_path', is => 'ro', default => '/api/balanceintervals/');
class_has('relation', is => 'ro', default => 'http://purl.org/sipwise/ngcp-api/#rel-balanceintervals');
#class_has(@{ __PACKAGE__->get_journal_query_params() });
class_has 'query_params' => (
is => 'ro',
isa => 'ArrayRef',
default => sub {[
{
param => 'start',
description => 'Filter balance intervals starting after or with the specified time stamp.',
query => {
first => sub {
my $q = shift;
my $dt = NGCP::Panel::Utils::DateTime::from_string($q);
return { 'start' => { '>=' => $dt } };
},
second => sub { },
},
},
#the end value of intervals is not constant along the retrieval operations
]},
);
__PACKAGE__->config(
action => {
(map { $_ => {
ACLDetachTo => '/api/root/invalid_user',
AllowedRole => [qw/admin reseller/],
Args => 1,
Does => [qw(ACL RequireSSL)],
Method => $_,
Path => __PACKAGE__->dispatch_path,
} } @{ __PACKAGE__->allowed_methods }),
item_base => {
Chained => '/',
PathPart => 'api/' . __PACKAGE__->resource_name,
CaptureArgs => 1,
},
item_get => {
Chained => 'item_base',
PathPart => '',
Args => 1,
Method => 'GET',
ACLDetachTo => '/api/root/invalid_user',
AllowedRole => [qw/admin reseller/],
Does => [qw(ACL RequireSSL)]
},
item_options => {
Chained => 'item_base',
PathPart => '',
Args => 1,
Method => 'OPTIONS',
ACLDetachTo => '/api/root/invalid_user',
AllowedRole => [qw/admin reseller/],
Does => [qw(ACL RequireSSL)]
},
item_head => {
Chained => 'item_base',
PathPart => '',
Args => 1,
Method => 'OPTIONS',
ACLDetachTo => '/api/root/invalid_user',
AllowedRole => [qw/admin reseller/],
Does => [qw(ACL RequireSSL)]
},
},
action_roles => [qw(HTTPMethods)],
);
sub auto :Private {
my ($self, $c) = @_;
$self->set_body($c);
$self->log_request($c);
#$self->apply_fake_time($c);
}
sub GET :Allow {
my ($self, $c, $id) = @_;
my $page = $c->request->params->{page} // 1;
my $rows = $c->request->params->{rows} // 10;
$c->model('DB')->set_transaction_isolation('READ COMMITTED');
my $guard = $c->model('DB')->txn_scope_guard;
{
last unless $self->valid_id($c, $id);
my $contract = $self->contract_by_id($c, $id);
last unless $self->resource_exists($c, contract => $contract);
my $balances = $self->balances_rs($c,$contract);
(my $total_count, $balances) = $self->paginate_order_collection($c, $balances);
my (@embedded, @links);
my $form = $self->get_form($c);
for my $balance ($balances->all) {
my $hal = $self->hal_from_balance($c, $balance, $form);
$hal->_forcearray(1);
push @embedded, $hal;
my $link = Data::HAL::Link->new(
relation => 'ngcp:'.$self->resource_name,
href => sprintf('/%s%d', $c->request->path, $balance->id),
);
$link->_forcearray(1);
push @links, $link;
}
$guard->commit;
push @links,
Data::HAL::Link->new(
relation => 'curies',
href => 'http://purl.org/sipwise/ngcp-api/#rel-{rel}',
name => 'ngcp',
templated => true,
),
Data::HAL::Link->new(relation => 'profile', href => 'http://purl.org/sipwise/ngcp-api/');
push @links, $self->collection_nav_links($page, $rows, $total_count, $c->request->path, $c->request->query_params);
my $hal = Data::HAL->new(
embedded => [@embedded],
links => [@links],
);
$hal->resource({
total_count => $total_count,
});
my $response = HTTP::Response->new(HTTP_OK, undef,
HTTP::Headers->new($hal->http_headers(skip_links => 1)), $hal->as_json);
$c->response->headers($response->headers);
$c->response->body($response->content);
return;
}
return;
}
sub HEAD :Allow {
my ($self, $c, $id) = @_;
$c->forward(qw(GET));
$c->response->body(q());
return;
}
sub OPTIONS :Allow {
my ($self, $c, $id) = @_;
my $allowed_methods = $self->allowed_methods_filtered($c);
$c->response->headers(HTTP::Headers->new(
Allow => $allowed_methods->join(', '),
Accept_Patch => 'application/json-patch+json',
));
$c->response->content_type('application/json');
$c->response->body(JSON::to_json({ methods => $allowed_methods })."\n");
return;
}
sub item_base {
my ($self,$c,$id) = @_;
$c->stash->{contract_id} = $id;
return undef;
}
sub item_get {
my ($self,$c,$id) = @_;
$c->model('DB')->set_transaction_isolation('READ COMMITTED');
my $guard = $c->model('DB')->txn_scope_guard;
{
my $contract_id = $c->stash->{contract_id};
last unless $self->valid_id($c, $contract_id);
my $contract = $self->contract_by_id($c, $contract_id);
last unless $self->resource_exists($c, contract => $contract);
my $balance = undef;
#if (API_JOURNALITEMTOP_RESOURCE_NAME and $id eq API_JOURNALITEMTOP_RESOURCE_NAME) {
# $balance = $self->balance_by_id($c,$contract_id);
#} els
if ($self->valid_id($c, $id)) {
$balance = $self->balance_by_id($c,$contract,$id);
} else {
last;
}
last unless $self->resource_exists($c, balanceinterval => $balance);
my $hal = $self->hal_from_balance($c,$balance);
$guard->commit;
#my $response = HTTP::Response->new(HTTP_OK, undef, HTTP::Headers->new(
# (map { # XXX Data::HAL must be able to generate links with multiple relations
# s|rel="(http://purl.org/sipwise/ngcp-api/#rel-resellers)"|rel="item $1"|;
# s/rel=self/rel="item self"/;
# $_
# } $hal->http_headers),
#), $hal->as_json);
$c->response->headers(HTTP::Headers->new($hal->http_headers));
$c->response->body($hal->as_json);
return;
}
return;
}
sub item_options {
my ($self, $c, $id) = @_;
my $allowed_methods = [ 'GET', 'HEAD', 'OPTIONS' ];
$c->response->headers(HTTP::Headers->new(
Allow => $allowed_methods->join(', '),
Accept_Patch => 'application/json-patch+json',
));
$c->response->content_type('application/json');
$c->response->body(JSON::to_json({ methods => $allowed_methods })."\n");
return;
}
sub item_head {
my ($self, $c, $id) = @_;
$c->forward('item_get');
$c->response->body(q());
return;
}
sub end : Private {
my ($self, $c) = @_;
#$self->reset_fake_time($c);
$self->log_response($c);
}
# vim: set tabstop=4 expandtab:

@ -9,6 +9,7 @@ use HTTP::Status qw(:constants);
use MooseX::ClassAttribute qw(class_has);
use NGCP::Panel::Utils::DateTime;
use NGCP::Panel::Utils::Contract;
use NGCP::Panel::Utils::ProfilePackages qw();
use Path::Tiny qw(path);
BEGIN { extends 'Catalyst::Controller::ActionRole'; }
require Catalyst::ActionRole::ACL;
@ -88,6 +89,7 @@ sub auto :Private {
$self->set_body($c);
$self->log_request($c);
#$self->apply_fake_time($c);
return 1;
}
@ -95,18 +97,25 @@ sub GET :Allow {
my ($self, $c) = @_;
my $page = $c->request->params->{page} // 1;
my $rows = $c->request->params->{rows} // 10;
$c->model('DB')->set_transaction_isolation('READ COMMITTED');
my $guard = $c->model('DB')->txn_scope_guard;
{
my $contracts = $self->item_rs($c);
my $contracts = $self->item_rs($c)->search_rs(undef,{
for => 'update',
});
(my $total_count, $contracts) = $self->paginate_order_collection($c, $contracts);
my $now = NGCP::Panel::Utils::DateTime::current_local;
my (@embedded, @links);
my $form = $self->get_form($c);
for my $contract ($contracts->all) {
push @embedded, $self->hal_from_contract($c, $contract, $form);
#NGCP::Panel::Utils::ProfilePackages::get_contract_balance
push @embedded, $self->hal_from_contract($c, $contract, $form, $now);
push @links, Data::HAL::Link->new(
relation => 'ngcp:'.$self->resource_name,
href => sprintf('/%s%d', $c->request->path, $contract->id),
);
}
$self->delay_commit($c,$guard); #potential db write ops in hal_from
push @links,
Data::HAL::Link->new(
relation => 'curies',
@ -114,15 +123,18 @@ sub GET :Allow {
name => 'ngcp',
templated => true,
),
Data::HAL::Link->new(relation => 'profile', href => 'http://purl.org/sipwise/ngcp-api/'),
Data::HAL::Link->new(relation => 'self', href => sprintf('/%s?page=%s&rows=%s', $c->request->path, $page, $rows));
Data::HAL::Link->new(relation => 'profile', href => 'http://purl.org/sipwise/ngcp-api/');
push @links, $self->collection_nav_links($page, $rows, $total_count, $c->request->path, $c->request->query_params);
# Data::HAL::Link->new(relation => 'self', href => sprintf('/%s?page=%s&rows=%s', $c->request->path, $page, $rows));
if(($total_count / $rows) > $page ) {
push @links, Data::HAL::Link->new(relation => 'next', href => sprintf('/%s?page=%d&rows=%d', $c->request->path, $page + 1, $rows));
}
if($page > 1) {
push @links, Data::HAL::Link->new(relation => 'prev', href => sprintf('/%s?page=%d&rows=%d', $c->request->path, $page - 1, $rows));
}
#if(($total_count / $rows) > $page ) {
# push @links, Data::HAL::Link->new(relation => 'next', href => sprintf('/%s?page=%d&rows=%d', $c->request->path, $page + 1, $rows));
#}
#if($page > 1) {
# push @links, Data::HAL::Link->new(relation => 'prev', href => sprintf('/%s?page=%d&rows=%d', $c->request->path, $page - 1, $rows));
#}
my $hal = Data::HAL->new(
embedded => [@embedded],
@ -221,12 +233,15 @@ sub POST :Allow {
foreach my $mapping (@$mappings_to_create) {
$contract->billing_mappings->create($mapping);
}
$contract = $self->contract_by_id($c, $contract->id,1);
NGCP::Panel::Utils::Contract::create_contract_balance(
c => $c,
profile => $contract->billing_mappings->find($contract->get_column('bmid'))->billing_profile, #$billing_profile,
$contract = $self->contract_by_id($c, $contract->id,1,$now);
NGCP::Panel::Utils::ProfilePackages::create_initial_contract_balance(schema => $schema,
contract => $contract,
);
profile => $contract->billing_mappings->find($contract->get_column('bmid'))->billing_profile,);
#NGCP::Panel::Utils::Contract::create_contract_balance(
# c => $c,
# profile => $contract->billing_mappings->find($contract->get_column('bmid'))->billing_profile, #$billing_profile,
# contract => $contract,
#);
} catch($e) {
$c->log->error("failed to create contract: $e"); # TODO: user, message, trace, ...
$self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Failed to create contract.");
@ -237,7 +252,7 @@ sub POST :Allow {
my $self = shift;
my ($c) = @_;
my $_contract = $self->contract_by_id($c, $contract->id, 1);
return $self->hal_from_contract($c,$_contract,$form); });
return $self->hal_from_contract($c,$_contract,$form,$now); });
$guard->commit;
@ -251,6 +266,7 @@ sub POST :Allow {
sub end : Private {
my ($self, $c) = @_;
#$self->reset_fake_time($c);
$self->log_response($c);
return;
}

@ -49,17 +49,21 @@ sub auto :Private {
$self->set_body($c);
$self->log_request($c);
#$self->apply_fake_time($c);
return 1;
}
sub GET :Allow {
my ($self, $c, $id) = @_;
$c->model('DB')->set_transaction_isolation('READ COMMITTED');
my $guard = $c->model('DB')->txn_scope_guard;
{
last unless $self->valid_id($c, $id);
my $contract = $self->contract_by_id($c, $id);
last unless $self->resource_exists($c, contract => $contract);
my $hal = $self->hal_from_contract($c, $contract);
my $hal = $self->hal_from_contract($c, $contract, undef, NGCP::Panel::Utils::DateTime::current_local);
$guard->commit; #potential db write ops in hal_from
my $response = HTTP::Response->new(HTTP_OK, undef, HTTP::Headers->new(
(map { # XXX Data::HAL must be able to generate links with multiple relations
@ -95,6 +99,7 @@ sub OPTIONS :Allow {
sub PATCH :Allow {
my ($self, $c, $id) = @_;
$c->model('DB')->set_transaction_isolation('READ COMMITTED');
my $guard = $c->model('DB')->txn_scope_guard;
{
my $preference = $self->require_preference($c);
@ -107,7 +112,8 @@ sub PATCH :Allow {
);
last unless $json;
my $contract = $self->contract_by_id($c, $id);
my $now = NGCP::Panel::Utils::DateTime::current_local;
my $contract = $self->contract_by_id($c, $id, $now);
last unless $self->resource_exists($c, contract => $contract);
my $old_resource = { $contract->get_inflated_columns };
@ -126,10 +132,10 @@ sub PATCH :Allow {
last unless $resource;
my $form = $self->get_form($c);
$contract = $self->update_contract($c, $contract, $old_resource, $resource, $form);
$contract = $self->update_contract($c, $contract, $old_resource, $resource, $form, $now);
last unless $contract;
my $hal = $self->hal_from_contract($c, $contract, $form);
my $hal = $self->hal_from_contract($c, $contract, $form, $now);
last unless $self->add_update_journal_item_hal($c,$hal);
$guard->commit;
@ -153,12 +159,14 @@ sub PATCH :Allow {
sub PUT :Allow {
my ($self, $c, $id) = @_;
$c->model('DB')->set_transaction_isolation('READ COMMITTED');
my $guard = $c->model('DB')->txn_scope_guard;
{
my $preference = $self->require_preference($c);
last unless $preference;
my $contract = $self->contract_by_id($c, $id);
my $now = NGCP::Panel::Utils::DateTime::current_local;
my $contract = $self->contract_by_id($c, $id, $now);
last unless $self->resource_exists($c, contract => $contract);
my $resource = $self->get_valid_put_data(
c => $c,
@ -171,10 +179,10 @@ sub PUT :Allow {
$old_resource->{type} = $billing_mapping->product->class;
my $form = $self->get_form($c);
$contract = $self->update_contract($c, $contract, $old_resource, $resource, $form);
$contract = $self->update_contract($c, $contract, $old_resource, $resource, $form, $now);
last unless $contract;
my $hal = $self->hal_from_contract($c, $contract, $form);
my $hal = $self->hal_from_contract($c, $contract, $form, $now);
last unless $self->add_update_journal_item_hal($c,$hal);
$guard->commit;
@ -264,6 +272,7 @@ sub journalsitem_head :Journal {
sub end : Private {
my ($self, $c) = @_;
#$self->reset_fake_time($c);
$self->log_response($c);
return;
}

@ -68,25 +68,32 @@ sub auto :Private {
$self->set_body($c);
$self->log_request($c);
#$self->apply_fake_time($c);
}
sub GET :Allow {
my ($self, $c) = @_;
my $page = $c->request->params->{page} // 1;
my $rows = $c->request->params->{rows} // 10;
$c->model('DB')->set_transaction_isolation('READ COMMITTED');
my $guard = $c->model('DB')->txn_scope_guard;
{
my $items = $self->item_rs($c);
my $items = $self->item_rs($c)->search_rs(undef,{
for => 'update',
});
(my $total_count, $items) = $self->paginate_order_collection($c, $items);
my $now = NGCP::Panel::Utils::DateTime::current_local;
my (@embedded, @links);
my $form = $self->get_form($c);
for my $item ($items->all) {
my $balance = $self->item_by_id($c, $item->id);
my $balance = $self->item_by_id($c, $item->id,$now);
push @embedded, $self->hal_from_item($c, $balance, $form);
push @links, Data::HAL::Link->new(
relation => 'ngcp:'.$self->resource_name,
href => sprintf('/%s%d', $c->request->path, $item->id),
);
}
$self->delay_commit($c,$guard);
push @links,
Data::HAL::Link->new(
relation => 'curies',
@ -94,14 +101,16 @@ sub GET :Allow {
name => 'ngcp',
templated => true,
),
Data::HAL::Link->new(relation => 'profile', href => 'http://purl.org/sipwise/ngcp-api/'),
Data::HAL::Link->new(relation => 'self', href => sprintf('/%s?page=%s&rows=%s', $c->request->path, $page, $rows));
if(($total_count / $rows) > $page ) {
push @links, Data::HAL::Link->new(relation => 'next', href => sprintf('/%s?page=%d&rows=%d', $c->request->path, $page + 1, $rows));
}
if($page > 1) {
push @links, Data::HAL::Link->new(relation => 'prev', href => sprintf('/%s?page=%d&rows=%d', $c->request->path, $page - 1, $rows));
}
Data::HAL::Link->new(relation => 'profile', href => 'http://purl.org/sipwise/ngcp-api/');
push @links, $self->collection_nav_links($page, $rows, $total_count, $c->request->path, $c->request->query_params);
# Data::HAL::Link->new(relation => 'self', href => sprintf('/%s?page=%s&rows=%s', $c->request->path, $page, $rows));
#if(($total_count / $rows) > $page ) {
# push @links, Data::HAL::Link->new(relation => 'next', href => sprintf('/%s?page=%d&rows=%d', $c->request->path, $page + 1, $rows));
#}
#if($page > 1) {
# push @links, Data::HAL::Link->new(relation => 'prev', href => sprintf('/%s?page=%d&rows=%d', $c->request->path, $page - 1, $rows));
#}
my $hal = Data::HAL->new(
embedded => [@embedded],
@ -141,6 +150,7 @@ sub OPTIONS :Allow {
sub end : Private {
my ($self, $c) = @_;
#$self->reset_fake_time($c);
$self->log_response($c);
}

@ -45,16 +45,20 @@ sub auto :Private {
$self->set_body($c);
$self->log_request($c);
#$self->apply_fake_time($c);
}
sub GET :Allow {
my ($self, $c, $id) = @_;
$c->model('DB')->set_transaction_isolation('READ COMMITTED');
my $guard = $c->model('DB')->txn_scope_guard;
{
last unless $self->valid_id($c, $id);
my $item = $self->item_by_id($c, $id);
last unless $self->resource_exists($c, customerbalance => $item);
my $hal = $self->hal_from_item($c, $item);
$guard->commit;
my $response = HTTP::Response->new(HTTP_OK, undef, HTTP::Headers->new(
(map { # XXX Data::HAL must be able to generate links with multiple relations
@ -91,6 +95,7 @@ sub OPTIONS :Allow {
sub PATCH :Allow {
my ($self, $c, $id) = @_;
$c->model('DB')->set_transaction_isolation('READ COMMITTED');
my $guard = $c->model('DB')->txn_scope_guard;
{
my $preference = $self->require_preference($c);
@ -137,6 +142,7 @@ sub PATCH :Allow {
sub PUT :Allow {
my ($self, $c, $id) = @_;
$c->model('DB')->set_transaction_isolation('READ COMMITTED');
my $guard = $c->model('DB')->txn_scope_guard;
{
my $preference = $self->require_preference($c);
@ -216,6 +222,7 @@ sub journalsitem_head :Journal {
sub end : Private {
my ($self, $c) = @_;
#$self->reset_fake_time($c);
$self->log_response($c);
}

@ -9,6 +9,7 @@ use HTTP::Status qw(:constants);
use MooseX::ClassAttribute qw(class_has);
use NGCP::Panel::Utils::DateTime;
use NGCP::Panel::Utils::Contract;
use NGCP::Panel::Utils::ProfilePackages qw();
use Path::Tiny qw(path);
BEGIN { extends 'Catalyst::Controller::ActionRole'; }
require Catalyst::ActionRole::ACL;
@ -125,6 +126,7 @@ sub auto :Private {
$self->set_body($c);
$self->log_request($c);
#$self->apply_fake_time($c);
return 1;
}
@ -132,18 +134,24 @@ sub GET :Allow {
my ($self, $c) = @_;
my $page = $c->request->params->{page} // 1;
my $rows = $c->request->params->{rows} // 10;
$c->model('DB')->set_transaction_isolation('READ COMMITTED');
my $guard = $c->model('DB')->txn_scope_guard;
{
my $customers = $self->item_rs($c);
my $customers = $self->item_rs($c)->search_rs(undef,{
for => 'update',
});
(my $total_count, $customers) = $self->paginate_order_collection($c, $customers);
my $now = NGCP::Panel::Utils::DateTime::current_local;
my (@embedded, @links);
my $form = $self->get_form($c);
for my $customer($customers->all) {
push @embedded, $self->hal_from_customer($c, $customer, $form);
push @embedded, $self->hal_from_customer($c, $customer, $form, $now);
push @links, Data::HAL::Link->new(
relation => 'ngcp:'.$self->resource_name,
href => sprintf('/%s%d', $c->request->path, $customer->id),
);
}
$self->delay_commit($c,$guard); #potential db write ops in hal_from
push @links,
Data::HAL::Link->new(
relation => 'curies',
@ -272,12 +280,15 @@ sub POST :Allow {
foreach my $mapping (@$mappings_to_create) {
$customer->billing_mappings->create($mapping);
}
$customer = $self->customer_by_id($c, $customer->id);
NGCP::Panel::Utils::Contract::create_contract_balance(
c => $c,
profile => $customer->billing_mappings->find($customer->get_column('bmid'))->billing_profile,
$customer = $self->customer_by_id($c, $customer->id,$now);
NGCP::Panel::Utils::ProfilePackages::create_initial_contract_balance(schema => $schema,
contract => $customer,
);
profile => $customer->billing_mappings->find($customer->get_column('bmid'))->billing_profile,);
#NGCP::Panel::Utils::Contract::create_contract_balance(
# c => $c,
# profile => $customer->billing_mappings->find($customer->get_column('bmid'))->billing_profile,
# contract => $customer,
#);
} catch($e) {
$c->log->error("failed to create customer contract: $e"); # TODO: user, message, trace, ...
$self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Failed to create customer.");
@ -288,7 +299,7 @@ sub POST :Allow {
my $self = shift;
my ($c) = @_;
my $_customer = $self->customer_by_id($c, $customer->id);
return $self->hal_from_customer($c,$_customer,$form); }); #$form
return $self->hal_from_customer($c,$_customer,$form, $now); }); #$form
$guard->commit;
@ -302,6 +313,7 @@ sub POST :Allow {
sub end : Private {
my ($self, $c) = @_;
#$self->reset_fake_time($c);
$self->log_response($c);
return;
}

@ -49,16 +49,20 @@ sub auto :Private {
$self->set_body($c);
$self->log_request($c);
#$self->apply_fake_time($c);
}
sub GET :Allow {
my ($self, $c, $id) = @_;
$c->model('DB')->set_transaction_isolation('READ COMMITTED');
my $guard = $c->model('DB')->txn_scope_guard;
{
last unless $self->valid_id($c, $id);
my $customer = $self->customer_by_id($c, $id);
last unless $self->resource_exists($c, customer => $customer);
my $hal = $self->hal_from_customer($c, $customer);
my $hal = $self->hal_from_customer($c, $customer, undef, NGCP::Panel::Utils::DateTime::current_local);
$guard->commit; #potential db write ops in hal_from
my $response = HTTP::Response->new(HTTP_OK, undef, HTTP::Headers->new(
(map { # XXX Data::HAL must be able to generate links with multiple relations
@ -95,6 +99,7 @@ sub OPTIONS :Allow {
sub PATCH :Allow {
my ($self, $c, $id) = @_;
$c->model('DB')->set_transaction_isolation('READ COMMITTED');
my $guard = $c->model('DB')->txn_scope_guard;
{
my $preference = $self->require_preference($c);
@ -107,7 +112,8 @@ sub PATCH :Allow {
);
last unless $json;
my $customer = $self->customer_by_id($c, $id);
my $now = NGCP::Panel::Utils::DateTime::current_local;
my $customer = $self->customer_by_id($c, $id, $now);
last unless $self->resource_exists($c, customer => $customer);
my $old_resource = { $customer->get_inflated_columns };
@ -129,10 +135,10 @@ sub PATCH :Allow {
last unless $resource;
my $form = $self->get_form($c);
$customer = $self->update_customer($c, $customer, $old_resource, $resource, $form);
$customer = $self->update_customer($c, $customer, $old_resource, $resource, $form, $now);
last unless $customer;
my $hal = $self->hal_from_customer($c, $customer, $form);
my $hal = $self->hal_from_customer($c, $customer, $form, $now);
last unless $self->add_update_journal_item_hal($c,$hal);
$guard->commit;
@ -156,12 +162,14 @@ sub PATCH :Allow {
sub PUT :Allow {
my ($self, $c, $id) = @_;
$c->model('DB')->set_transaction_isolation('READ COMMITTED');
my $guard = $c->model('DB')->txn_scope_guard;
{
my $preference = $self->require_preference($c);
last unless $preference;
my $customer = $self->customer_by_id($c, $id);
my $now = NGCP::Panel::Utils::DateTime::current_local;
my $customer = $self->customer_by_id($c, $id, $now);
last unless $self->resource_exists($c, customer => $customer);
my $resource = $self->get_valid_put_data(
c => $c,
@ -172,10 +180,10 @@ sub PUT :Allow {
my $old_resource = { $customer->get_inflated_columns };
my $form = $self->get_form($c);
$customer = $self->update_customer($c, $customer, $old_resource, $resource, $form);
$customer = $self->update_customer($c, $customer, $old_resource, $resource, $form, $now);
last unless $customer;
my $hal = $self->hal_from_customer($c, $customer, $form);
my $hal = $self->hal_from_customer($c, $customer, $form,$now);
last unless $self->add_update_journal_item_hal($c,$hal);
$guard->commit;
@ -263,6 +271,7 @@ sub journalsitem_head :Journal {
sub end : Private {
my ($self, $c) = @_;
#$self->reset_fake_time($c);
$self->log_response($c);
}

@ -330,7 +330,7 @@ sub get_collection_properties {
#}
} elsif($f->type =~ /Select$/) {
$type = $self->field_to_select_options($f);
} elsif($f->type !~ /Regex|EmailList|Identifier|PosInteger/) {
} elsif($f->type !~ /Regex|EmailList|Identifier|PosInteger|DateTime/) { #Interval, IPAddress, ...?
$name .= '_id';
}
} elsif ($f->$_isa('HTML::FormHandler::Field::Select')) {

@ -145,7 +145,7 @@ sub POST :Allow {
used_at => $now,
});
} catch($e) {
$c->log->error("failed to create vouche topup: $e"); # TODO: user, message, trace, ...
$c->log->error("failed to create voucher topup: $e"); # TODO: user, message, trace, ...
$self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Failed to create voucher topup.");
last;
}

@ -7,6 +7,7 @@ use NGCP::Panel::Form::Contract::PeeringReseller;
use NGCP::Panel::Utils::Message;
use NGCP::Panel::Utils::Navigation;
use NGCP::Panel::Utils::Contract;
use NGCP::Panel::Utils::ProfilePackages;
use NGCP::Panel::Utils::Subscriber;
use NGCP::Panel::Utils::DateTime;
@ -29,8 +30,9 @@ sub contract_list :Chained('/') :PathPart('contract') :CaptureArgs(0) {
{ name => "status", search => 1, title => $c->loc("Status") },
]);
my $now = NGCP::Panel::Utils::DateTime::current_local;
my $rs = NGCP::Panel::Utils::Contract::get_contract_rs(
schema => $c->model('DB'));
schema => $c->model('DB'),now => $now);
unless($c->user->is_superuser) {
$rs = $rs->search({
'contact.reseller_id' => $c->user->reseller_id,
@ -46,7 +48,7 @@ sub contract_list :Chained('/') :PathPart('contract') :CaptureArgs(0) {
],
});
$c->stash(contract_select_rs => $rs);
$c->stash(now => $now);
$c->stash(ajax_uri => $c->uri_for_action("/contract/ajax"));
$c->stash(template => 'contract/list.tt');
}
@ -92,7 +94,7 @@ sub base :Chained('contract_list') :PathPart('') :CaptureArgs(1) {
$billing_mapping->product->handle ne 'PSTN_PEERING')) {
}
my $now = NGCP::Panel::Utils::DateTime::current_local;
my $now = $c->stash->{now};
my $billing_mappings_ordered = NGCP::Panel::Utils::Contract::billing_mappings_ordered($contract_rs->first->billing_mappings,$now,$contract_first->get_column('bmid'));
my $future_billing_mappings = NGCP::Panel::Utils::Contract::billing_mappings_ordered(NGCP::Panel::Utils::Contract::future_billing_mappings($contract_rs->first->billing_mappings,$now));
@ -100,7 +102,7 @@ sub base :Chained('contract_list') :PathPart('') :CaptureArgs(1) {
$c->stash(contract_rs => $contract_rs);
$c->stash(billing_mapping => $billing_mapping );
$c->stash(billing_mappings_ordered_result => $billing_mappings_ordered ); # all billings mappings are displayed in the details page
$c->stash(now => $now);
$c->stash(future_billing_mappings => $future_billing_mappings ); # only editable billing mappings are displayed in the edit dialog
return;
}
@ -155,6 +157,7 @@ sub edit :Chained('base') :PathPart('edit') :Args(0) {
if($posted && $form->validated) {
try {
my $schema = $c->model('DB');
$schema->set_transaction_isolation('READ COMMITTED');
$schema->txn_do(sub {
foreach(qw/contact billing_profile/){
$form->values->{$_.'_id'} = $form->values->{$_}{id} || undef;
@ -177,6 +180,7 @@ sub edit :Chained('base') :PathPart('edit') :Args(0) {
});
my $old_status = $contract->status;
my $old_package = $contract->profile_package;
$contract->update($form->values);
NGCP::Panel::Utils::Contract::remove_future_billing_mappings($contract,$now) if $delete_mappings;
@ -185,6 +189,15 @@ sub edit :Chained('base') :PathPart('edit') :Args(0) {
}
$contract = $c->stash->{contract_rs}->first;
#$billing_mapping = $contract->billing_mappings->find($contract->get_column('bmid'));
my $balance = NGCP::Panel::Utils::ProfilePackages::catchup_contract_balances(c => $c,
contract => $contract,
old_package => $old_package,);
$balance = NGCP::Panel::Utils::ProfilePackages::resize_actual_contract_balance(c => $c,
contract => $contract,
old_package => $old_package,
balance => $balance,
);
if ($is_peering_reseller &&
defined $contract->contact->reseller_id) {
@ -356,11 +369,14 @@ sub peering_create :Chained('peering_list') :PathPart('create') :Args(0) {
'+as' => 'bmid',
})->first;
NGCP::Panel::Utils::Contract::create_contract_balance(
c => $c,
profile => $contract->billing_mappings->find($contract->get_column('bmid'))->billing_profile, #$billing_profile,
NGCP::Panel::Utils::ProfilePackages::create_initial_contract_balance(schema => $schema,
contract => $contract,
);
profile => $contract->billing_mappings->find($contract->get_column('bmid'))->billing_profile,);
#NGCP::Panel::Utils::Contract::create_contract_balance(
# c => $c,
# profile => $contract->billing_mappings->find($contract->get_column('bmid'))->billing_profile, #$billing_profile,
# contract => $contract,
#);
if (defined $contract->contact->reseller_id) {
my $contact_id = $contract->contact->id;
@ -426,9 +442,9 @@ sub reseller_ajax_contract_filter :Chained('reseller_list') :PathPart('ajax/cont
$c->response->redirect($c->uri_for());
return;
}
my $now = $c->stash->{now};
my $rs = NGCP::Panel::Utils::Contract::get_contract_rs(
schema => $c->model('DB'))
schema => $c->model('DB'), now => $now)
->search_rs({
'me.id' => $contract_id,
});
@ -501,11 +517,14 @@ sub reseller_create :Chained('reseller_list') :PathPart('create') :Args(0) {
'+as' => 'bmid',
})->first;
NGCP::Panel::Utils::Contract::create_contract_balance(
c => $c,
profile => $contract->billing_mappings->find($contract->get_column('bmid'))->billing_profile, #$billing_profile,
NGCP::Panel::Utils::ProfilePackages::create_initial_contract_balance(schema => $schema,
contract => $contract,
);
profile => $contract->billing_mappings->find($contract->get_column('bmid'))->billing_profile,);
#NGCP::Panel::Utils::Contract::create_contract_balance(
# c => $c,
# profile => $contract->billing_mappings->find($contract->get_column('bmid'))->billing_profile, #$billing_profile,
# contract => $contract,
#);
if (defined $contract->contact->reseller_id) {
my $contact_id = $contract->contact->id;

@ -6,7 +6,7 @@ use JSON qw(decode_json encode_json);
use IPC::System::Simple qw/capturex EXIT_ANY $EXITVAL/;
use NGCP::Panel::Form::CustomerMonthlyFraud;
use NGCP::Panel::Form::CustomerDailyFraud;
use NGCP::Panel::Form::CustomerBalance;
use NGCP::Panel::Form::Balance::CustomerBalance;
use NGCP::Panel::Form::Customer::Subscriber;
use NGCP::Panel::Form::Customer::PbxAdminSubscriber;
use NGCP::Panel::Form::Customer::PbxExtensionSubscriber;
@ -23,6 +23,7 @@ use NGCP::Panel::Utils::DateTime;
use NGCP::Panel::Utils::Subscriber;
use NGCP::Panel::Utils::Sounds;
use NGCP::Panel::Utils::Contract;
use NGCP::Panel::Utils::ProfilePackages;
use NGCP::Panel::Utils::DeviceBootstrap;
use Template;
@ -58,11 +59,14 @@ sub list_customer :Chained('/') :PathPart('customer') :CaptureArgs(0) {
{ name => "status", search => 1, title => $c->loc("Status") },
{ name => "max_subscribers", search => 1, title => $c->loc("Max Number of Subscribers") },
]);
my $rs = NGCP::Panel::Utils::Contract::get_customer_rs(c => $c);
my $now = NGCP::Panel::Utils::DateTime::current_local;
my $rs = NGCP::Panel::Utils::Contract::get_customer_rs(c => $c, now => $now);
$c->stash(
contract_select_rs => $rs,
template => 'customer/list.tt'
template => 'customer/list.tt',
now => $now
);
}
@ -178,11 +182,14 @@ sub create :Chained('list_customer') :PathPart('create') :Args(0) {
'+as' => 'bmid',
})->first;
NGCP::Panel::Utils::Contract::create_contract_balance(
c => $c,
profile => $contract->billing_mappings->find($contract->get_column('bmid'))->billing_profile, #$billing_profile,
NGCP::Panel::Utils::ProfilePackages::create_initial_contract_balance(schema => $schema,
contract => $contract,
);
profile => $contract->billing_mappings->find($contract->get_column('bmid'))->billing_profile,);
#NGCP::Panel::Utils::Contract::create_contract_balance(
# c => $c,
# profile => $contract->billing_mappings->find($contract->get_column('bmid'))->billing_profile, #$billing_profile,
# contract => $contract,
#);
$c->session->{created_objects}->{contract} = { id => $contract->id };
delete $c->session->{created_objects}->{contact};
delete $c->session->{created_objects}->{billing_profile};
@ -252,23 +259,20 @@ sub base :Chained('list_customer') :PathPart('') :CaptureArgs(1) {
NGCP::Panel::Utils::Navigation::back_or($c, $c->uri_for('/customer'));
}
my $now = NGCP::Panel::Utils::DateTime::current_local;
my $now = $c->stash->{now};
my $billing_mappings_ordered = NGCP::Panel::Utils::Contract::billing_mappings_ordered($contract_rs->first->billing_mappings,$now,$contract_rs->first->get_column('bmid'));
my $future_billing_mappings = NGCP::Panel::Utils::Contract::billing_mappings_ordered(NGCP::Panel::Utils::Contract::future_billing_mappings($contract_rs->first->billing_mappings,$now));
my $billing_mapping = $contract_rs->first->billing_mappings->find($contract_rs->first->get_column('bmid'));
my $stime = $now->clone->truncate(to => 'month');
my $etime = $stime->clone->add(months => 1)->subtract(seconds => 1);
my $balance;
try {
$balance = NGCP::Panel::Utils::Contract::get_contract_balance(
c => $c,
profile => $billing_mapping->billing_profile,
contract => $contract_rs->first,
stime => $stime,
etime => $etime
);
my $schema = $c->model('DB');
$schema->set_transaction_isolation('READ COMMITTED');
$schema->txn_do(sub {
$balance = NGCP::Panel::Utils::ProfilePackages::get_contract_balance(c => $c,
contract => $contract_rs->first,
now => $now);
});
} catch($e) {
NGCP::Panel::Utils::Message->error(
c => $c,
@ -276,8 +280,28 @@ sub base :Chained('list_customer') :PathPart('') :CaptureArgs(1) {
desc => $c->loc('Failed to get contract balance.'),
);
$c->response->redirect($c->uri_for());
return;
return;
}
#my $stime = $now->clone->truncate(to => 'month');
#my $etime = $stime->clone->add(months => 1)->subtract(seconds => 1);
#my $balance;
#try {
# $balance = NGCP::Panel::Utils::Contract::get_contract_balance(
# c => $c,
# profile => $billing_mapping->billing_profile,
# contract => $contract_rs->first,
# stime => $stime,
# etime => $etime
# );
#} catch($e) {
# NGCP::Panel::Utils::Message->error(
# c => $c,
# error => $e,
# desc => $c->loc('Failed to get contract balance.'),
# );
# $c->response->redirect($c->uri_for());
# return;
#}
my $product_id = $contract_rs->first->get_column('product_id');
NGCP::Panel::Utils::Message->error(
@ -369,7 +393,7 @@ sub base :Chained('list_customer') :PathPart('') :CaptureArgs(1) {
$c->stash(contract => $contract_first);
$c->stash(contract_rs => $contract_rs);
$c->stash(billing_mapping => $billing_mapping );
$c->stash(now => $now );
#$c->stash(now => $now );
$c->stash(billing_mappings_ordered_result => $billing_mappings_ordered );
$c->stash(future_billing_mappings => $future_billing_mappings );
}
@ -425,6 +449,7 @@ sub edit :Chained('base_restricted') :PathPart('edit') :Args(0) {
if($posted && $form->validated) {
try {
my $schema = $c->model('DB');
$schema->set_transaction_isolation('READ COMMITTED');
$schema->txn_do(sub {
foreach(qw/contact billing_profile profile_package product subscriber_email_template passreset_email_template invoice_email_template invoice_template/){
$form->values->{$_.'_id'} = $form->values->{$_}{id} || undef;
@ -453,6 +478,7 @@ sub edit :Chained('base_restricted') :PathPart('edit') :Args(0) {
my $old_prepaid = $billing_mapping->billing_profile->prepaid;
my $old_ext_id = $contract->external_id // '';
my $old_status = $contract->status;
my $old_package = $contract->profile_package;
$contract->update($form->values);
NGCP::Panel::Utils::Contract::remove_future_billing_mappings($contract,$now) if $delete_mappings;
@ -462,6 +488,15 @@ sub edit :Chained('base_restricted') :PathPart('edit') :Args(0) {
$contract = $c->stash->{contract_rs}->first;
$billing_mapping = $contract->billing_mappings->find($contract->get_column('bmid'));
$billing_profile = $billing_mapping->billing_profile;
my $balance = NGCP::Panel::Utils::ProfilePackages::catchup_contract_balances(c => $c,
contract => $contract,
old_package => $old_package,);
$balance = NGCP::Panel::Utils::ProfilePackages::resize_actual_contract_balance(c => $c,
contract => $contract,
old_package => $old_package,
balance => $balance,
);
my $new_ext_id = $contract->external_id // '';
@ -873,8 +908,10 @@ sub edit_balance :Chained('base_restricted') :PathPart('balance/edit') :Args(0)
my ($self, $c) = @_;
my $balance = $c->stash->{balance};
my $contract = $c->stash->{contract};
my $now = $c->stash->{now};
my $posted = ($c->request->method eq 'POST');
my $form = NGCP::Panel::Form::CustomerBalance->new;
my $form = NGCP::Panel::Form::Balance::CustomerBalance->new;
my $params = { $balance->get_inflated_columns };
# cash_balance => $balance->cash_balance,
# free_time_balance => $balance->free_time_balance,
@ -893,7 +930,14 @@ sub edit_balance :Chained('base_restricted') :PathPart('balance/edit') :Args(0)
);
if($posted && $form->validated) {
try {
$balance->update($form->values);
my $schema = $c->model('DB');
$schema->set_transaction_isolation('READ COMMITTED');
$schema->txn_do(sub {
$balance = NGCP::Panel::Utils::ProfilePackages::get_contract_balance(c => $c,
contract => $contract,
now => $now);
$balance->update($form->values);
});
NGCP::Panel::Utils::Message->info(
c => $c,
desc => $c->loc('Account balance successfully changed!'),
@ -906,11 +950,11 @@ sub edit_balance :Chained('base_restricted') :PathPart('balance/edit') :Args(0)
desc => $c->loc('Failed to change account balance!'),
);
}
$c->response->redirect($c->uri_for_action("/customer/details", [$c->stash->{contract}->id]));
$c->response->redirect($c->uri_for_action("/customer/details", [$contract->id]));
return;
}
$c->stash(close_target => $c->uri_for_action("/customer/details", [$c->stash->{contract}->id]));
$c->stash(close_target => $c->uri_for_action("/customer/details", [$contract->id]));
$c->stash(form => $form);
$c->stash(edit_flag => 1);
}

@ -6,6 +6,7 @@ BEGIN { extends 'Catalyst::Controller'; }
use NGCP::Panel::Utils::Message;
use NGCP::Panel::Utils::DateTime;
use NGCP::Panel::Utils::Contract;
use NGCP::Panel::Utils::ProfilePackages;
use NGCP::Panel::Utils::InvoiceTemplate;
use NGCP::Panel::Utils::Invoice;
use NGCP::Panel::Form::Invoice::Invoice;
@ -63,7 +64,8 @@ sub customer_inv_list :Chained('/') :PathPart('invoice/customer') :CaptureArgs(1
error => "Invalid contract id $contract_id found",
desc => $c->loc('Invalid contract id found'),
);
NGCP::Panel::Utils::Navigation::back_or($c, $c->uri_for('/sound'));
#NGCP::Panel::Utils::Navigation::back_or($c, $c->uri_for('/sound'));
NGCP::Panel::Utils::Navigation::back_or($c, $c->uri_for('/invoice'));
}
if($c->user->roles eq "subscriberadmin" && $c->user->account_id != $contract_id) {
NGCP::Panel::Utils::Message->error(
@ -71,7 +73,8 @@ sub customer_inv_list :Chained('/') :PathPart('invoice/customer') :CaptureArgs(1
error => "access violation, subscriberadmin ".$c->user->uuid." with contract id ".$c->user->account_id." tries to access foreign contract id $contract_id",
desc => $c->loc('Invalid contract id found'),
);
NGCP::Panel::Utils::Navigation::back_or($c, $c->uri_for('/sound'));
#NGCP::Panel::Utils::Navigation::back_or($c, $c->uri_for('/sound'));
NGCP::Panel::Utils::Navigation::back_or($c, $c->uri_for('/invoice'));
}
my $contract = $c->model('DB')->resultset('contracts')->find($contract_id);
unless($contract) {
@ -80,13 +83,15 @@ sub customer_inv_list :Chained('/') :PathPart('invoice/customer') :CaptureArgs(1
error => "Contract id $contract_id not found",
desc => $c->loc('Invalid contract id detected'),
);
NGCP::Panel::Utils::Navigation::back_or($c, $c->uri_for('/sound'));
#NGCP::Panel::Utils::Navigation::back_or($c, $c->uri_for('/sound'));
NGCP::Panel::Utils::Navigation::back_or($c, $c->uri_for('/invoice'));
}
$c->stash(inv_rs => $c->model('DB')->resultset('invoices')->search({
contract_id => $contract->id,
}));
$c->stash(template => 'sound/list.tt');
#$c->stash(template => 'sound/list.tt');
$c->stash(template => 'invoice/invoice_list.tt');
return;
}
@ -155,6 +160,7 @@ sub create :Chained('inv_list') :PathPart('create') :Args() :Does(ACL) :ACLDetac
if($posted && $form->validated) {
try {
my $schema = $c->model('DB');
$schema->set_transaction_isolation('READ COMMITTED');
$schema->txn_do(sub {
my $contract_id = $form->values->{contract}{id};
my $customer_rs = NGCP::Panel::Utils::Contract::get_customer_rs(c => $c);
@ -213,6 +219,16 @@ sub create :Chained('inv_list') :PathPart('create') :Args() :Does(ACL) :ACLDetac
delete $form->values->{period}
)->truncate(to => 'month');
my $etime = $stime->clone->add(months => 1)->subtract(seconds => 1);
#this has to be refactored - select a contract balance instead of a "period"
my $balance = NGCP::Panel::Utils::ProfilePackages::get_contract_balance(c => $c,
contract => $customer,
stime => $stime,
etime => $etime,);
$stime = $balance->start;
$etime = $balance->end;
my $bm_actual = NGCP::Panel::Utils::ProfilePackages::get_actual_billing_mapping(c => $c, contract => $customer, now => $balance->start);
my $billing_profile = $bm_actual->billing_mappings->first->billing_profile;
my $zonecalls = NGCP::Panel::Utils::Contract::get_contract_zonesfees(
c => $c,
@ -235,27 +251,25 @@ sub create :Chained('inv_list') :PathPart('create') :Args() :Does(ACL) :ACLDetac
$call->{source_customer_cost} += 0.0; # make sure it's a number
$call;
} $calllist_rs->all ];
my $billing_mapping = $customer->billing_mappings->find($customer->get_column('bmid'));
my $billing_profile = $billing_mapping->billing_profile;
my $balance;
try {
$balance = NGCP::Panel::Utils::Contract::get_contract_balance(
c => $c,
profile => $billing_profile,
contract => $customer,
stime => $stime,
etime => $etime
);
} catch($e) {
NGCP::Panel::Utils::Message->error(
c => $c,
error => $e,
desc => $c->loc('Failed to get contract balance.'),
);
die;
}
#my $billing_mapping = $customer->billing_mappings->find($customer->get_column('bmid'));
#my $billing_profile = $billing_mapping->billing_profile;
#try {
# $balance = NGCP::Panel::Utils::Contract::get_contract_balance(
# c => $c,
# profile => $billing_profile,
# contract => $customer,
# stime => $stime,
# etime => $etime
# );
#} catch($e) {
# NGCP::Panel::Utils::Message->error(
# c => $c,
# error => $e,
# desc => $c->loc('Failed to get contract balance.'),
# );
# die;
#}
my $invoice_amounts = NGCP::Panel::Utils::Invoice::get_invoice_amounts(
customer_contract => {$customer->get_inflated_columns},

@ -457,11 +457,14 @@ sub create_defaults :Path('create_defaults') :Args(0) :Does(ACL) :ACLDetachTo('/
%{ $defaults{admins} },
reseller_id => $r{resellers}->id,
});
NGCP::Panel::Utils::Contract::create_contract_balance(
c => $c,
profile => $r{billing_mappings}->find($r{contracts}->get_column('bmid'))->billing_profile, #$r{billing_mappings}->billing_profile,
NGCP::Panel::Utils::ProfilePackages::create_initial_contract_balance(c => $c,
contract => $r{contracts},
);
profile => $r{billing_mappings}->find($r{contracts}->get_column('bmid'))->billing_profile,);
#NGCP::Panel::Utils::Contract::create_contract_balance(
# c => $c,
# profile => $r{billing_mappings}->find($r{contracts}->get_column('bmid'))->billing_profile, #$r{billing_mappings}->billing_profile,
# contract => $r{contracts},
#);
});
} catch($e) {
NGCP::Panel::Utils::Message->error(

@ -1,11 +1,14 @@
package NGCP::Panel::Controller::Root;
use Sipwise::Base;
BEGIN { extends 'Catalyst::Controller' }
use Scalar::Util qw(blessed);
use NGCP::Panel::Utils::DateTime qw();
use DateTime qw();
use Time::HiRes qw();
use DateTime::Format::RFC3339 qw();
use NGCP::Panel::Widget;
use Scalar::Util qw(blessed);
use Time::HiRes qw();
#
# Sets the actions in this controller to be registered with no prefix
@ -90,6 +93,7 @@ sub auto :Private {
}));
return;
}
$self->api_apply_fake_time($c);
return 1;
@ -118,6 +122,7 @@ sub auto :Private {
}));
return;
}
$self->api_apply_fake_time($c);
return 1;
}
}
@ -274,6 +279,28 @@ sub emptyajax :Chained('/') :PathPart('emptyajax') :Args(0) {
$c->detach( $c->view("JSON") );
}
sub api_apply_fake_time :Private {
my ($self, $c) = @_;
my $allow_fake_client_time = 0;
my $cfg = $c->config->{api_debug_opts};
$allow_fake_client_time = ((defined $cfg->{allow_fake_client_time}) && $cfg->{allow_fake_client_time} ? 1 : 0) if defined $cfg;
if ($allow_fake_client_time) { #exists $ENV{API_FAKE_CLIENT_TIME} && $ENV{API_FAKE_CLIENT_TIME}) {
my $date = $c->request->header('X-Fake-Clienttime'); #('Date');
if ($date) {
my $dt = NGCP::Panel::Utils::DateTime::from_rfc1123_string($date);
if ($dt) {
NGCP::Panel::Utils::DateTime::set_fake_time($dt);
$c->stash->{is_fake_time} = 1;
$c->log->debug('using X-Fake-Clienttime header to fake system time: ' . NGCP::Panel::Utils::DateTime::to_string(NGCP::Panel::Utils::DateTime::current_local));
return;
}
}
NGCP::Panel::Utils::DateTime::set_fake_time();
$c->stash->{is_fake_time} = 0;
#$c->log->debug('resetting faked system time: ' . NGCP::Panel::Utils::DateTime::to_string(NGCP::Panel::Utils::DateTime::current_local));
}
}
__PACKAGE__->meta->make_immutable;
1;

@ -0,0 +1,19 @@
package NGCP::Panel::Field::ContractBalance;
use HTML::FormHandler::Moose;
extends 'HTML::FormHandler::Field::Compound';
has_field 'id' => (
type => '+NGCP::Panel::Field::DataTable',
label => 'Balance Interval',
do_label => 0,
do_wrapper => 0,
required => 1,
template => 'helpers/datatables_field.tt',
ajax_src => '/somewhere/ajax',
table_titles => ['#', 'bla', 'blah'],
table_fields => ['id', 'x', 'y'],
);
1;
# vim: set tabstop=4 expandtab:

@ -0,0 +1,93 @@
package NGCP::Panel::Form::Balance::BalanceIntervalAPI;
use HTML::FormHandler::Moose;
use HTML::FormHandler::Widget::Block::Bootstrap;
use Moose::Util::TypeConstraints;
extends 'HTML::FormHandler';
has_field 'id' => (
type => 'Hidden',
);
has_field 'start' => (
type => '+NGCP::Panel::Field::DateTime',
required => 0,
element_attr => {
rel => ['tooltip'],
title => ['The datetime (YYYY-MM-DD HH:mm:ss) pointing the first second belonging to the balance interval.']
},
);
has_field 'stop' => (
type => '+NGCP::Panel::Field::DateTime',
required => 0,
element_attr => {
rel => ['tooltip'],
title => ['The datetime (YYYY-MM-DD HH:mm:ss) pointing the last second belonging to the balance interval.']
},
);
has_field 'billing_profile_id' => (
type => 'PosInteger',
#required => 1,
element_attr => {
rel => ['tooltip'],
title => ['The id of the billing profile at the first second of the balance interval.']
},
);
has_field 'invoice_id' => (
type => 'PosInteger',
#required => 1,
element_attr => {
rel => ['tooltip'],
title => ['The id of the invoice containing this invoice.']
},
);
has_field 'cash_balance' => (
type => 'Money',
#label => 'Cash Balance',
#required => 1,
#inflate_method => sub { return $_[1] * 100 },
#deflate_method => sub { return $_[1] / 100 },
element_attr => {
rel => ['tooltip'],
title => ['The interval\'s cash balance of the contract in EUR/USD/etc.']
},
);
has_field 'cash_debit' => (
type => 'Money',
#label => 'Cash Balance',
#required => 1,
#inflate_method => sub { return $_[1] * 100 },
#deflate_method => sub { return $_[1] / 100 },
element_attr => {
rel => ['tooltip'],
title => ['The amount spent during this interval in EUR/USD/etc.']
},
);
has_field 'free_time_balance' => (
type => 'Integer',
#label => 'Free-Time Balance',
#required => 1,
element_attr => {
rel => ['tooltip'],
title => ['The interval\'s free-time balance of the contract in seconds.']
},
);
has_field 'free_time_spent' => (
type => 'Integer',
#label => 'Free-Time Balance',
#required => 1,
element_attr => {
rel => ['tooltip'],
title => ['The free-time spent during this interval in EUR/USD/etc.']
},
);
1;
# vim: set tabstop=4 expandtab:

@ -1,4 +1,4 @@
package NGCP::Panel::Form::CustomerBalance;
package NGCP::Panel::Form::Balance::CustomerBalance;
use HTML::FormHandler::Moose;
extends 'HTML::FormHandler';
@ -56,7 +56,7 @@ has_block 'actions' => (
=head1 NAME
NGCP::Panel::Form::CustomerBalance
NGCP::Panel::Form::Balance::CustomerBalance
=head1 DESCRIPTION

@ -42,6 +42,16 @@ has_field 'period' => (
required => 1,
);
#has_field 'balance_interval' => (
# type => '+NGCP::Panel::Field::BalanceInterval',
# label => 'Balance Interval',
# validate_when_empty => 1,
# element_attr => {
# rel => ['tooltip'],
# title => ['The balance interval to create the invoice for.']
# },
#);
has_field 'save' => (
type => 'Submit',
value => 'Save',

@ -28,6 +28,17 @@ __PACKAGE__->config(
connect_info => [],
);
sub set_transaction_isolation {
my ($self,$level) = @_;
return $self->storage->dbh_do(
sub {
my ($storage, $dbh, @args) = @_;
$dbh->do("SET TRANSACTION ISOLATION LEVEL " . $args[0]);
},
$level,
);
}
=head1 NAME
NGCP::Panel::Model::DB - Catalyst DBIC Schema Model

@ -16,7 +16,7 @@ use Types::Standard qw(InstanceOf);
use Regexp::Common qw(delimited); # $RE{delimited}
use HTTP::Headers::Util qw(split_header_words);
use NGCP::Panel::Utils::ValidateJSON qw();
#use NGCP::Panel::Utils::DateTime qw();
use NGCP::Panel::Utils::Journal qw();
#use boolean qw(true);
#use Data::HAL qw();
@ -490,6 +490,34 @@ sub apply_patch {
return $entity;
}
#sub apply_fake_time {
# my ($self, $c) = @_;
# if (exists $ENV{API_FAKE_CLIENT_TIME} && $ENV{API_FAKE_CLIENT_TIME}) {
# my $date = $c->request->header('Date');
# if ($date) {
# my $dt = NGCP::Panel::Utils::DateTime::from_rfc1123_string($date);
# if ($dt) {
# NGCP::Panel::Utils::DateTime::set_fake_time($dt->epoch);
# $c->stash->{is_fake_time} = 1;
# $c->log('using date header to fake system time: ' . NGCP::Panel::Utils::DateTime::to_string(NGCP::Panel::Utils::DateTime::current_local));
# return;
# }
# }
# NGCP::Panel::Utils::DateTime::set_fake_time();
# $c->stash->{is_fake_time} = 0;
# $c->log('resetting faked system time: ' . NGCP::Panel::Utils::DateTime::to_string(NGCP::Panel::Utils::DateTime::current_local));
# }
#}
#sub reset_fake_time {
# my ($self, $c) = @_;
# if (exists $ENV{API_FAKE_CLIENT_TIME} && $ENV{API_FAKE_CLIENT_TIME} && $c->stash->{fake_time}) {
# NGCP::Panel::Utils::DateTime::set_fake_time();
# $c->stash->{fake_time} = 0;
# $c->log('resetting faked system time: ' . NGCP::Panel::Utils::DateTime::to_string(NGCP::Panel::Utils::DateTime::current_local));
# }
#}
sub set_body {
my ($self, $c) = @_;
#Ctx could be initialized in Root::get_collections - wouldn't it be better?
@ -611,6 +639,21 @@ sub is_false {
return;
}
sub delay_commit {
my ($self, $c, $guard) = @_;
my $allow_delay_commit = 0;
my $cfg = $c->config->{api_debug_opts};
$allow_delay_commit = ((defined $cfg->{allow_delay_commit}) && $cfg->{allow_delay_commit} ? 1 : 0) if defined $cfg;
if ($allow_delay_commit) {
my $delay = $c->request->header('X-Delay-Commit'); #('Expect');
if ($delay && $delay =~ /\d+/ && $delay > 0 && $delay < 500) {
$c->log->debug('using X-Delay-Commit header to delay db commit for ' . $delay . ' seconds');
sleep($delay);
}
}
$guard->commit();
}
sub add_create_journal_item_hal {
my ($self,$c,@args) = @_;
return NGCP::Panel::Utils::Journal::add_journal_item_hal($self,$c,NGCP::Panel::Utils::Journal::CREATE_JOURNAL_OP,@args);

@ -0,0 +1,154 @@
package NGCP::Panel::Role::API::BalanceIntervals;
use Moose::Role;
use Sipwise::Base;
with 'NGCP::Panel::Role::API' => {
-alias =>{ item_rs => '_item_rs', },
-excludes => [ 'item_rs' ],
};
use boolean qw(true);
use TryCatch;
use Data::HAL qw();
use Data::HAL::Link qw();
use HTTP::Status qw(:constants);
use NGCP::Panel::Form::Balance::BalanceIntervalAPI;
use NGCP::Panel::Utils::Contract;
use NGCP::Panel::Utils::ProfilePackages qw();
use NGCP::Panel::Utils::DateTime;
sub _contract_rs {
my ($self, $c, $include_terminated,$now) = @_;
my $item_rs = NGCP::Panel::Utils::Contract::get_contract_rs(
schema => $c->model('DB'),
include_terminated => (defined $include_terminated && $include_terminated ? 1 : 0),
now => $now,
);
if($c->user->roles eq "admin") {
} elsif($c->user->roles eq "reseller") {
$item_rs = $item_rs->search({
'contact.reseller_id' => $c->user->reseller_id
},{
join => 'contact',
});
}
return $item_rs;
#my $item_rs = $c->model('DB')->resultset('contract_balances');
#if($c->user->roles eq "admin") {
#} elsif($c->user->roles eq "reseller") {
# $item_rs = $item_rs->search({
# 'contact.reseller_id' => $c->user->reseller_id
# },{
# join => { contract => 'contact' },
# });
#}
#return $item_rs;
}
sub item_rs {
my $self = shift;
return $self->_contract_rs(@_);
}
sub get_form {
my ($self, $c) = @_;
return NGCP::Panel::Form::Balance::BalanceIntervalAPI->new;
}
sub hal_from_balance {
my ($self, $c, $item, $form, $use_root_collection_link) = @_;
my $contract = $item->contract;
my $is_customer = (defined $contract->contact->reseller_id ? 1 : 0);
my $bm_start = NGCP::Panel::Utils::ProfilePackages::get_actual_billing_mapping(c => $c, contract => $contract, now => $item->start);
my $profile_at_start = $bm_start->billing_mappings->first->billing_profile;
my $invoice = $item->invoice;
my %resource = $item->get_inflated_columns;
$resource{cash_balance} /= 100.0;
$resource{cash_debit} = (delete $resource{cash_balance_interval}) / 100.0;
$resource{free_time_spent} = delete $resource{free_time_interval};
my $datetime_fmt = DateTime::Format::Strptime->new(
pattern => '%F %T',
);
$resource{start} = delete $resource{start};
$resource{stop} = delete $resource{end};
$resource{start} = $datetime_fmt->format_datetime($resource{start}) if defined $resource{start};
$resource{stop} = $datetime_fmt->format_datetime($resource{stop}) if defined $resource{stop};
$resource{billing_profile_id} = $profile_at_start->id;
my $hal = Data::HAL->new(
links => [
Data::HAL::Link->new(
relation => 'curies',
href => 'http://purl.org/sipwise/ngcp-api/#rel-{rel}',
name => 'ngcp',
templated => true,
),
($use_root_collection_link ? Data::HAL::Link->new(relation => 'collection', href => sprintf("/api/%s/", $self->resource_name)) :
Data::HAL::Link->new(relation => 'collection', href => sprintf("/api/%s/%d/", $self->resource_name, $contract->id)) ),
Data::HAL::Link->new(relation => 'profile', href => 'http://purl.org/sipwise/ngcp-api/'),
Data::HAL::Link->new(relation => 'self', href => sprintf("/api/%s/%d/%d", $self->resource_name, $contract->id, $item->id)),
($is_customer ? ( Data::HAL::Link->new(relation => 'ngcp:customers', href => sprintf("/api/customers/%d", $contract->id)),
Data::HAL::Link->new(relation => 'ngcp:customerbalances', href => sprintf("/api/customerbalances/%d", $contract->id)) ) :
Data::HAL::Link->new(relation => 'ngcp:contracts', href => sprintf("/api/contracts/%d", $contract->id)) ),
Data::HAL::Link->new(relation => 'ngcp:billingprofiles', href => sprintf("/api/billingprofiles/%d", $profile_at_start->id)),
($invoice ? Data::HAL::Link->new(relation => 'ngcp:invoices', href => sprintf("/api/invoices/%d", $invoice->id)) : ()),
],
relation => 'ngcp:'.$self->resource_name,
);
$form //= $self->get_form($c);
$self->validate_form(
c => $c,
resource => \%resource,
form => $form,
run => 0,
exceptions => [ 'billing_profile_id', 'invoice_id' ],
);
#$resource{id} = int($item->contract->id);
$hal->resource({%resource});
return $hal;
}
sub contract_by_id {
my ($self, $c, $id) = @_;
return $self->_contract_rs($c)->find($id); #must not process item controller's query params
}
sub balances_rs {
my ($self, $c,$contract) = @_;
NGCP::Panel::Utils::ProfilePackages::catchup_contract_balances(c => $c,
contract => $contract,
now => NGCP::Panel::Utils::DateTime::current_local);
return $self->apply_query_params($c,$self->can('query_params') ? $self->query_params : {},$contract->contract_balances);
}
sub balance_by_id {
my ($self, $c, $contract, $id) = @_;
my $balance = NGCP::Panel::Utils::ProfilePackages::catchup_contract_balances(c => $c,
contract => $contract,
now => NGCP::Panel::Utils::DateTime::current_local);
if (defined $id) {
$balance = $contract->contract_balances->find($id);
}
return $balance;
}
1;
# vim: set tabstop=4 expandtab:

@ -13,14 +13,16 @@ use Data::HAL::Link qw();
use HTTP::Status qw(:constants);
use NGCP::Panel::Utils::DateTime;
use NGCP::Panel::Utils::Contract;
use NGCP::Panel::Utils::ProfilePackages qw();
use NGCP::Panel::Form::Contract::ContractAPI qw();
sub item_rs {
my ($self, $c, $include_terminated) = @_;
my ($self, $c, $include_terminated,$now) = @_;
my $item_rs = NGCP::Panel::Utils::Contract::get_contract_rs(
schema => $c->model('DB'),
include_terminated => (defined $include_terminated && $include_terminated ? 1 : 0),
now => $now,
);
$item_rs = $item_rs->search({
'contact.reseller_id' => undef
@ -39,36 +41,41 @@ sub get_form {
}
sub hal_from_contract {
my ($self, $c, $contract, $form) = @_;
my ($self, $c, $contract, $form, $now) = @_;
my $billing_mapping = $contract->billing_mappings->find($contract->get_column('bmid'));
my $billing_profile_id = $billing_mapping->billing_profile_id;
my $future_billing_profiles = NGCP::Panel::Utils::Contract::resource_from_future_mappings($contract);
my $billing_profiles = NGCP::Panel::Utils::Contract::resource_from_mappings($contract);
my $stime = NGCP::Panel::Utils::DateTime::current_local()->truncate(to => 'month');
my $etime = $stime->clone->add(months => 1);
my $contract_balance = $contract->contract_balances
->find({
start => { '>=' => $stime },
end => { '<' => $etime },
});
unless($contract_balance) {
try {
NGCP::Panel::Utils::Contract::create_contract_balance(
c => $c,
profile => $billing_mapping->billing_profile,
contract => $contract,
);
} catch($e) {
$c->log->error("Failed to create current contract balance for contract id '".$contract->id."': $e");
$self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Internal Server Error.");
return;
}
$contract_balance = $contract->contract_balances->find({
start => { '>=' => $stime },
end => { '<' => $etime },
});
}
#my $stime = NGCP::Panel::Utils::DateTime::current_local()->truncate(to => 'month');
#my $etime = $stime->clone->add(months => 1);
#my $contract_balance = $contract->contract_balances
# ->find({
# start => { '>=' => $stime },
# end => { '<' => $etime },
# });
#unless($contract_balance) {
# try {
# NGCP::Panel::Utils::Contract::create_contract_balance(
# c => $c,
# profile => $billing_mapping->billing_profile,
# contract => $contract,
# );
# } catch($e) {
# $c->log->error("Failed to create current contract balance for contract id '".$contract->id."': $e");
# $self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Internal Server Error.");
# return;
# }
# $contract_balance = $contract->contract_balances->find({
# start => { '>=' => $stime },
# end => { '<' => $etime },
# });
#}
#we leave this here to keep the former behaviour: contract balances are also created upon GET api/contracts/4711
NGCP::Panel::Utils::ProfilePackages::catchup_contract_balances(c => $c,
contract => $contract,
now => $now);
my %resource = $contract->get_inflated_columns;
@ -121,17 +128,19 @@ sub hal_from_contract {
}
sub contract_by_id {
my ($self, $c, $id, $include_terminated) = @_;
my $item_rs = $self->item_rs($c,$include_terminated);
my ($self, $c, $id, $include_terminated, $now) = @_;
my $item_rs = $self->item_rs($c,$include_terminated,$now);
return $item_rs->find($id);
}
sub update_contract {
my ($self, $c, $contract, $old_resource, $resource, $form) = @_;
my ($self, $c, $contract, $old_resource, $resource, $form, $now) = @_;
my $billing_mapping = $contract->billing_mappings->find($contract->get_column('bmid'));
my $billing_profile = $billing_mapping->billing_profile;
my $old_package = $contract->profile_package;
$form //= $self->get_form($c);
# TODO: for some reason, formhandler lets missing contact_id slip thru
$resource->{contact_id} //= undef;
@ -143,7 +152,7 @@ sub update_contract {
exceptions => [ "contact_id", "billing_profile_id" ],
);
my $now = NGCP::Panel::Utils::DateTime::current_local;
#my $now = NGCP::Panel::Utils::DateTime::current_local;
my $mappings_to_create = [];
my $delete_mappings = 0;
@ -182,9 +191,18 @@ sub update_contract {
foreach my $mapping (@$mappings_to_create) {
$contract->billing_mappings->create($mapping);
}
$contract = $self->contract_by_id($c, $contract->id,1);
$contract = $self->contract_by_id($c, $contract->id,1,$now);
$billing_mapping = $contract->billing_mappings->find($contract->get_column('bmid'));
$billing_profile = $billing_mapping->billing_profile;
$billing_profile = $billing_mapping->billing_profile;
my $balance = NGCP::Panel::Utils::ProfilePackages::catchup_contract_balances(c => $c,
contract => $contract,
old_package => $old_package,);
$balance = NGCP::Panel::Utils::ProfilePackages::resize_actual_contract_balance(c => $c,
contract => $contract,
old_package => $old_package,
balance => $balance,
);
if($old_resource->{status} ne $resource->{status}) {
if($contract->id == 1) {

@ -11,8 +11,9 @@ use TryCatch;
use Data::HAL qw();
use Data::HAL::Link qw();
use HTTP::Status qw(:constants);
use NGCP::Panel::Form::CustomerBalance;
use NGCP::Panel::Form::Balance::CustomerBalance;
use NGCP::Panel::Utils::Contract;
use NGCP::Panel::Utils::ProfilePackages qw();
use NGCP::Panel::Utils::DateTime;
sub item_rs {
@ -32,12 +33,12 @@ sub item_rs {
sub get_form {
my ($self, $c) = @_;
return NGCP::Panel::Form::CustomerBalance->new;
return NGCP::Panel::Form::Balance::CustomerBalance->new;
}
sub hal_from_item {
my ($self, $c, $item, $form) = @_;
my %resource = $item->get_inflated_columns;
$resource{cash_balance} /= 100;
@ -73,33 +74,36 @@ sub hal_from_item {
}
sub item_by_id {
my ($self, $c, $id) = @_;
my ($self, $c, $id, $now) = @_;
my $stime = NGCP::Panel::Utils::DateTime::current_local()->truncate(to => 'month');
my $etime = $stime->clone->add(months => 1)->subtract(seconds => 1);
#my $stime = NGCP::Panel::Utils::DateTime::current_local()->truncate(to => 'month');
#my $etime = $stime->clone->add(months => 1)->subtract(seconds => 1);
my $item_rs = $self->item_rs($c);
$item_rs = $item_rs
->search({
'me.id' => $id,
},{
'+select' => 'billing_mappings.id',
'+as' => 'bmid',
});
my $item = $item_rs->first;
my $billing_mapping = $item->billing_mappings->find($item->get_column('bmid'));
my $balance = NGCP::Panel::Utils::Contract::get_contract_balance(
c => $c,
contract => $item,
profile => $billing_mapping->billing_profile,
stime => $stime,
etime => $etime,
);
return $balance;
#my $item_rs = $self->item_rs($c);
#$item_rs = $item_rs
# ->search({
# 'me.id' => $id,
# },{
# '+select' => 'billing_mappings.id',
# '+as' => 'bmid',
# });
return NGCP::Panel::Utils::ProfilePackages::get_contract_balance(c => $c,
contract => $self->item_rs($c)->find($id),
now => $now);
#my $item = $item_rs->first;
#my $billing_mapping = $item->billing_mappings->find($item->get_column('bmid'));
#my $balance = NGCP::Panel::Utils::Contract::get_contract_balance(
# c => $c,
# contract => $item,
# profile => $billing_mapping->billing_profile,
# stime => $stime,
# etime => $etime,
#);
#return $balance;
}
sub update_item {

@ -13,16 +13,18 @@ use Data::HAL::Link qw();
use HTTP::Status qw(:constants);
use NGCP::Panel::Utils::DateTime;
use NGCP::Panel::Utils::Contract;
use NGCP::Panel::Utils::ProfilePackages qw();
use NGCP::Panel::Utils::Preferences;
use NGCP::Panel::Form::Contract::CustomerAPI qw();
sub item_rs {
my ($self, $c) = @_;
my ($self, $c, $now) = @_;
# returns a contracts rs filtered based on role
my $item_rs = NGCP::Panel::Utils::Contract::get_customer_rs(
c => $c,
include_terminated => 1,
now => $now,
);
return $item_rs;
}
@ -33,36 +35,41 @@ sub get_form {
}
sub hal_from_customer {
my ($self, $c, $customer, $form) = @_;
my ($self, $c, $customer, $form, $now) = @_;
my $billing_mapping = $customer->billing_mappings->find($customer->get_column('bmid'));
my $billing_profile_id = $billing_mapping->billing_profile->id;
my $future_billing_profiles = NGCP::Panel::Utils::Contract::resource_from_future_mappings($customer);
my $billing_profiles = NGCP::Panel::Utils::Contract::resource_from_mappings($customer);
my $stime = NGCP::Panel::Utils::DateTime::current_local()->truncate(to => 'month');
my $etime = $stime->clone->add(months => 1);
my $contract_balance = $customer->contract_balances
->find({
start => { '>=' => $stime },
end => { '<' => $etime },
});
unless($contract_balance) {
try {
NGCP::Panel::Utils::Contract::create_contract_balance(
c => $c,
profile => $billing_mapping->billing_profile,
contract => $customer,
);
} catch($e) {
$c->log->error("Failed to create current contract balance for customer contract id '".$customer->id."': $e");
$self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Internal Server Error.");
return;
};
$contract_balance = $customer->contract_balances->find({
start => { '>=' => $stime },
end => { '<' => $etime },
});
}
#my $stime = NGCP::Panel::Utils::DateTime::current_local()->truncate(to => 'month');
#my $etime = $stime->clone->add(months => 1);
#my $contract_balance = $customer->contract_balances
# ->find({
# start => { '>=' => $stime },
# end => { '<' => $etime },
# });
#unless($contract_balance) {
# try {
# NGCP::Panel::Utils::Contract::create_contract_balance(
# c => $c,
# profile => $billing_mapping->billing_profile,
# contract => $customer,
# );
# } catch($e) {
# $c->log->error("Failed to create current contract balance for customer contract id '".$customer->id."': $e");
# $self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Internal Server Error.");
# return;
# };
# $contract_balance = $customer->contract_balances->find({
# start => { '>=' => $stime },
# end => { '<' => $etime },
# });
#}
#we leave this here to keep the former behaviour: contract balances are also created upon GET api/customers/4711
NGCP::Panel::Utils::ProfilePackages::catchup_contract_balances(c => $c,
contract => $customer,
now => $now);
my %resource = $customer->get_inflated_columns;
@ -125,13 +132,13 @@ sub hal_from_customer {
}
sub customer_by_id {
my ($self, $c, $id) = @_;
my $customers = $self->item_rs($c);
my ($self, $c, $id, $now) = @_;
my $customers = $self->item_rs($c,$now);
return $customers->find($id);
}
sub update_customer {
my ($self, $c, $customer, $old_resource, $resource, $form) = @_;
my ($self, $c, $customer, $old_resource, $resource, $form, $now) = @_;
if ($customer->status eq 'terminated') {
$self->error($c, HTTP_UNPROCESSABLE_ENTITY, 'Customer is already terminated and cannot be changed.');
@ -140,6 +147,9 @@ sub update_customer {
my $billing_mapping = $customer->billing_mappings->find($customer->get_column('bmid'));
my $billing_profile = $billing_mapping->billing_profile;
my $old_package = $customer->profile_package;
$old_resource->{prepaid} = $billing_profile->prepaid;
$form //= $self->get_form($c);
@ -153,7 +163,7 @@ sub update_customer {
exceptions => [ "contact_id", "billing_profile_id", "profile_package_id" ],
);
my $now = NGCP::Panel::Utils::DateTime::current_local;
#my $now = NGCP::Panel::Utils::DateTime::current_local;
my $mappings_to_create = [];
my $delete_mappings = 0;
@ -246,9 +256,18 @@ sub update_customer {
foreach my $mapping (@$mappings_to_create) {
$customer->billing_mappings->create($mapping);
}
$customer = $self->customer_by_id($c, $customer->id);
$customer = $self->customer_by_id($c, $customer->id, $now);
$billing_mapping = $customer->billing_mappings->find($customer->get_column('bmid'));
$billing_profile = $billing_mapping->billing_profile;
$billing_profile = $billing_mapping->billing_profile;
my $balance = NGCP::Panel::Utils::ProfilePackages::catchup_contract_balances(c => $c,
contract => $customer,
old_package => $old_package,);
$balance = NGCP::Panel::Utils::ProfilePackages::resize_actual_contract_balance(c => $c,
contract => $customer,
old_package => $old_package,
balance => $balance,
);
if(($customer->external_id // '') ne $old_ext_id) {
foreach my $sub($customer->voip_subscribers->all) {

@ -7,107 +7,101 @@ 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');
my $package;
if (defined $contract->contact->reseller_id && ($package = $contract->profile_package)) {
}
# 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 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 = @_;
@ -210,13 +204,14 @@ sub recursively_lock_contract {
sub get_contract_rs {
my %params = @_;
my $schema = $params{schema};
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(NGCP::Panel::Utils::DateTime::current_local) ) x 2],
bind => [ ( $dtf->format_datetime($now) ) x 2],
'join' => { 'billing_mappings_actual' => { 'billing_mappings' => 'product'}},
'+select' => [
'billing_mappings.id',
@ -236,7 +231,7 @@ sub get_contract_rs {
sub get_customer_rs {
my %params = @_;
my $c = $params{c};
my ($c,$now) = @params{qw/c now/};
my $customers = get_contract_rs(
schema => $c->model('DB'),

@ -1,8 +1,15 @@
package NGCP::Panel::Utils::DateTime;
use Sipwise::Base;
#use Sipwise::Base; seg fault when creating threads in test scripts
use strict;
use warnings;
use Time::Fake; #load this before any use DateTime
use DateTime;
#use DateTime::Infinite;
use DateTime::Format::ISO8601;
use DateTime::Format::Strptime;
use constant RFC_1123_FORMAT_PATTERN => '%a, %d %b %Y %T %Z';
sub current_local {
return DateTime->now(
@ -10,6 +17,48 @@ sub current_local {
);
}
sub infinite_past {
#mysql 5.5: The supported range is '1000-01-01 00:00:00' ...
return DateTime->new(year => 1000, month => 1, day => 1, hour => 0, minute => 0, second => 0,
time_zone => DateTime::TimeZone->new(name => 'UTC')
);
#$dt->epoch calls should be okay if perl >= 5.12.0
}
sub infinite_future {
#... to '9999-12-31 23:59:59'
return DateTime->new(year => 9999, month => 12, day => 31, hour => 23, minute => 59, second => 59,
#applying the 'local' timezone takes too long -> "The current implementation of DateTime::TimeZone
#will use a huge amount of memory calculating all the DST changes from now until the future date.
#Use UTC or the floating time zone and you will be safe."
time_zone => DateTime::TimeZone->new(name => 'UTC')
#- with floating timezones, the long conversion takes place when comparing with a 'local' dt
#- the error due to leap years/seconds is not relevant in comparisons
);
}
sub set_fake_time {
my ($o) = @_;
if (defined $o) {
Time::Fake->offset(ref $o eq 'DateTime' ? $o->epoch : $o);
} else {
Time::Fake->reset();
}
}
#sub infinite_past {
# DateTime::Infinite::Past->new();
#}
#sub infinite_future {
# return DateTime::Infinite::Future->new();
#}
sub last_day_of_month {
my $dt = shift;
return DateTime->last_day_of_month(year => $dt->year, month => $dt->month,
time_zone => DateTime::TimeZone->new(name => 'local'))->day;
}
sub epoch_local {
my $epoch = shift;
return DateTime->from_epoch(
@ -32,6 +81,17 @@ sub from_string {
return $ts;
}
sub from_rfc1123_string {
my $s = shift;
my $strp = DateTime::Format::Strptime->new(pattern => RFC_1123_FORMAT_PATTERN,
on_error => 'undef');
return $strp->parse_datetime($s);
}
sub new_local {
my %params;
@params{qw/year month day hour minute second nanosecond/} = @_;
@ -65,6 +125,16 @@ sub to_string
return $s;
}
sub to_rfc1123_string {
my $dt = shift;
my $strp = DateTime::Format::Strptime->new(pattern => RFC_1123_FORMAT_PATTERN,
on_error => 'undef');
return $strp->format_datetime($dt);
}
1;

@ -78,7 +78,7 @@ sub validate_entities {
}
my $entity = undef;
eval {
$entity = (defined $field->{id} ? $schema->resultset($field->{resultset})->find($field->{id}) : undef);
$entity = (defined $field->{id} ? $schema->resultset($field->{resultset})->find({id => $field->{id}},($field->{lock} ? {for => 'update'} : undef)) : undef);
};
if ($@) {
return 0 unless &{$err_code}($@,$field->{hfh_field});

@ -249,6 +249,7 @@ sub handle_api_journals_get {
sub handle_api_journalsitem_get {
my ($controller,$c,$id) = @_;
my $guard = $c->model('DB')->txn_scope_guard;
{
my $item_id = $c->stash->{item_id_journal};
last unless $controller->valid_id($c, $item_id);
@ -266,10 +267,11 @@ sub handle_api_journalsitem_get {
if ($journal->resource_id != $item_id) {
$c->log->error("Journal item '" . $id . "' does not belong to '" . $controller->resource_name . '/' . $item_id . "'");
$controller->error($c, HTTP_NOT_FOUND, "Entity 'journal' not found.");
return;
last;
}
my $hal = hal_from_journal($controller,$c,$journal);
$guard->commit;
#my $response = HTTP::Response->new(HTTP_OK, undef, HTTP::Headers->new(
# (map { # XXX Data::HAL must be able to generate links with multiple relations

@ -3,6 +3,7 @@ package NGCP::Panel::Utils::Message;
use Catalyst;
use Sipwise::Base;
use Data::Dumper;
use Time::Fake; #load this before any use DateTime
use DateTime qw();
use DateTime::Format::RFC3339 qw();
use Time::HiRes qw();

@ -2,6 +2,9 @@ package NGCP::Panel::Utils::ProfilePackages;
use strict;
use warnings;
#use TryCatch;
use NGCP::Panel::Utils::DateTime;
use constant INITIAL_PROFILE_DISCRIMINATOR => 'initial';
use constant UNDERRUN_PROFILE_DISCRIMINATOR => 'underrun';
use constant TOPUP_PROFILE_DISCRIMINATOR => 'topup';
@ -10,7 +13,451 @@ use constant _DISCRIMINATOR_MAP => { initial_profiles => INITIAL_PROFILE_DISCRIM
underrun_profiles => UNDERRUN_PROFILE_DISCRIMINATOR,
topup_profiles => TOPUP_PROFILE_DISCRIMINATOR};
use constant _CARRY_OVER_TIMELY => 'carry_over_timely';
use constant _CARRY_OVER_TIMELY_MODE => 'carry_over_timely';
use constant _CARRY_OVER_MODE => 'carry_over';
use constant _DEFAULT_CARRY_OVER_MODE => _CARRY_OVER_MODE;
use constant _DEFAULT_INITIAL_BALANCE => 0.0;
use constant _TOPUP_START_MODE => 'topup';
use constant _1ST_START_MODE => '1st';
use constant _CREATE_START_MODE => 'create';
use constant _START_MODE_PRESERVE_EOM => { _TOPUP_START_MODE . '' => 0,
_1ST_START_MODE . '' => 0,
_CREATE_START_MODE . '' => 1};
use constant _DEFAULT_START_MODE => '1st';
use constant _DEFAULT_PROFILE_INTERVAL_UNIT => 'week';
use constant _DEFAULT_PROFILE_INTERVAL_COUNT => 1;
use constant _DEFAULT_PROFILE_FREE_TIME => 0;
use constant _DEFAULT_PROFILE_FREE_CASH => 0.0;
sub get_contract_balance {
my %params = @_;
my($c,$contract,$now,$schema,$stime,$etime) = @params{qw/c contract now schema stime etime/};
$schema //= $c->model('DB');
$now //= NGCP::Panel::Utils::DateTime::current_local;
my $balance = catchup_contract_balances(schema => $schema, contract => $contract, now => $now);
if (defined $stime || defined $etime) { #supported for backward compat only
$balance = $contract->contract_balances->search({
start => { '>=' => $stime },
end => { '<=' => $etime },
},{ order_by => { '-desc' => 'end'},})->first;
}
return $balance;
}
sub resize_actual_contract_balance {
my %params = @_;
my($c,$contract,$old_package,$actual_balance,$now,$schema) = @params{qw/c contract old_package balance now schema/};
$schema //= $c->model('DB');
$contract = $schema->resultset('contracts')->find({id => $contract->id},{for => 'update'}); #lock record
return $actual_balance unless defined $contract->contact->reseller_id;
$now //= $contract->modify_timestamp;
my $new_package = $contract->profile_package;
my ($old_start_mode,$new_start_mode);
if (defined $old_package && !defined $new_package) {
$old_start_mode = $old_package->balance_interval_start_mode if $old_package->balance_interval_start_mode ne _DEFAULT_START_MODE;
$new_start_mode = _DEFAULT_START_MODE;
} elsif (!defined $old_package && defined $new_package) {
$old_start_mode = _DEFAULT_START_MODE;
$new_start_mode = $new_package->balance_interval_start_mode if $new_package->balance_interval_start_mode ne _DEFAULT_START_MODE;
} elsif (defined $old_package && defined $new_package && $old_package->balance_interval_start_mode ne $new_package->balance_interval_start_mode) { #&& $old_package->id != $new_package->id ?
$old_start_mode = $old_package->balance_interval_start_mode;
$new_start_mode = $new_package->balance_interval_start_mode;
}
if ($old_start_mode && $new_start_mode) {
my $end_of_resized_interval = _get_resized_interval_end(ctime => $now,
create_timestamp => $contract->create_timestamp // $contract->modify_timestamp,
start_mode => $new_start_mode);
my $resized_balance_values = _get_resized_balance_values(schema => $schema,
balance => $actual_balance,
old_start_mode => $old_start_mode,
new_start_mode => $new_start_mode,
etime => $end_of_resized_interval);
#try {
# $schema->txn_do(sub {
$actual_balance->update({
end => $end_of_resized_interval,
@$resized_balance_values,
});
#catchup_contract_balances(schema => $schema,
# contract => $contract,
# old_package => $new_package,
# now => $now) if _TOPUP_START_MODE eq $new_package->start_mode;
# });
#} catch($e) {
# if ($e =~ /Duplicate entry/) {
# #libswrate or rat-o-mat are interferring?
# $c->log->warn("Resizing contract balance failed: Duplicate entry. Ignoring!");
# } else {
# $c->log->error("Resizing contract balance failed: " . $e);
# $e->rethrow;
# }
#};
}
return $actual_balance;
}
sub catchup_contract_balances {
my %params = @_;
my($c,$contract,$old_package,$now,$schema) = @params{qw/c contract old_package now schema/};
$schema //= $c->model('DB');
$contract = $schema->resultset('contracts')->find({id => $contract->id},{for => 'update'}); #lock record
$now //= $contract->modify_timestamp;
$old_package //= $contract->profile_package;
my ($start_mode,$interval_unit,$interval_value,$carry_over_mode,$has_package);
if (defined $contract->contact->reseller_id && $old_package) {
$start_mode = $old_package->balance_interval_start_mode;
$interval_unit = $old_package->balance_interval_unit;
$interval_value = $old_package->balance_interval_value;
$carry_over_mode = $old_package->carry_over_mode;
$has_package = 1;
} else {
$start_mode = _DEFAULT_START_MODE;
$carry_over_mode = _DEFAULT_CARRY_OVER_MODE;
$has_package = 0;
}
my $last_balance = $contract->contract_balances->search(undef,{ order_by => { '-desc' => 'end'},})->first;
my $last_profile;
#my $end_of_today = $now->clone->truncate(to => 'day')->add(days => 1)->subtract(seconds => 1);
while ($last_balance && $last_balance->end < $now) {
my $start_of_next_interval = $last_balance->end->clone->add(seconds => 1);
my $bm_actual;
unless ($last_profile) {
$bm_actual = get_actual_billing_mapping(schema => $schema, contract => $contract, now => $last_balance->start);
$last_profile = $bm_actual->billing_mappings->first->billing_profile;
}
$bm_actual = get_actual_billing_mapping(schema => $schema, contract => $contract, now => $start_of_next_interval);
my $profile = $bm_actual->billing_mappings->first->billing_profile;
$interval_unit = $has_package ? $interval_unit : ($profile->interval_unit // _DEFAULT_PROFILE_INTERVAL_UNIT);
$interval_value = $has_package ? $interval_value : ($profile->interval_count // _DEFAULT_PROFILE_INTERVAL_COUNT);
my ($stime,$etime) = _get_balance_interval_start_end(
last_etime => $last_balance->end,
start_mode => $start_mode,
#now => $start_of_next_interval,
interval_unit => $interval_unit,
interval_value => $interval_value,
create => $contract->create_timestamp // $contract->modify_timestamp);
my $balance_values = _get_balance_values(schema => $schema,
stime => $stime,
etime => $etime,
start_mode => $start_mode,
contract => $contract,
profile => $profile,
carry_over_mode => $carry_over_mode,
last_balance => $last_balance,
last_profile => $last_profile,
);
$last_profile = $profile;
#try {
# $schema->txn_do(sub {
$last_balance = $schema->resultset('contract_balances')->create({
contract_id => $contract->id,
start => $stime,
end => $etime,
@$balance_values,
});
$last_balance->discard_changes();
# });
#} catch($e) {
# if ($e =~ /Duplicate entry/) {
# #libswrate or rat-o-mat are interferring?
# $c->log->warn("Creating contract balance failed: Duplicate entry. Ignoring!");
# $last_balance = $contract->contract_balances
# ->find({
# start => { '>=' => $stime },
# end => { '<=' => $etime },
# });
# } else {
# $c->log->error("Creating contract balance failed: " . $e);
# $e->rethrow;
# }
#};
}
return $last_balance;
}
sub XXtopup_create_contract_balance {
my %params = @_;
my($c,$contract,$now,$profile,$schema) = @params{qw/c contract now profile schema/};
return create_initial_contract_balance(c => $c,contract => $contract,now => $now,$profile,$schema,is_topup => 1);
}
sub create_initial_contract_balance {
my %params = @_;
my($c,$contract,$profile,$now,$schema) = @params{qw/c contract profile now schema/};
$schema //= $c->model('DB');
$contract = $schema->resultset('contracts')->find({id => $contract->id},{for => 'update'}); #lock record
$now //= $contract->create_timestamp // $contract->modify_timestamp;
my ($start_mode,$interval_unit,$interval_value,$initial_balance);
my $package = $contract->profile_package;
if (defined $contract->contact->reseller_id && $package) {
$start_mode = $package->balance_interval_start_mode;
$interval_unit = $package->balance_interval_unit;
$interval_value = $package->balance_interval_value;
$initial_balance = $package->initial_balance; #euro
} else {
$start_mode = _DEFAULT_START_MODE;
$interval_unit = $profile->interval_unit // _DEFAULT_PROFILE_INTERVAL_UNIT; #'month';
$interval_value = $profile->interval_count // _DEFAULT_PROFILE_INTERVAL_COUNT; #1;
$initial_balance = _DEFAULT_INITIAL_BALANCE;
}
my ($stime,$etime) = _get_balance_interval_start_end(
now => $now,
start_mode => $start_mode,
interval_unit => $interval_unit,
interval_value => $interval_value,
create => $contract->create_timestamp // $contract->modify_timestamp);
my $balance_values = _get_balance_values(schema => $schema,
stime => $stime,
etime => $etime,
start_mode => $start_mode,
now => $now,
profile => $profile,
initial_balance => $initial_balance * 100.0,
);
#my $balance;
#try {
# $schema->txn_do(sub {
my $balance = $schema->resultset('contract_balances')->create({
contract_id => $contract->id,
start => $stime,
end => $etime,
@$balance_values,
});
$balance->discard_changes();
# });
#} 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_resized_balance_values {
my %params = @_;
my ($c,$balance,$old_start_mode,$new_start_mode,$etime,$schema) = @params{qw/c balance old_start_mode new_start_mode etime schema/};
$schema //= $c->model('DB');
my ($cash_balance, $free_time_balance) = ($balance->cash_balance,$balance->free_time_balance);
my $contract = $balance->contract;
my $contract_create = $contract->create_timestamp // $contract->modify_timestamp;
if ($balance->start <= $contract_create && $balance->end >= $contract_create) {
my $bm = get_actual_billing_mapping(schema => $schema, contract => $contract, now => $contract_create); #now => $balance->start); #end); !?
my $profile = $bm->billing_mappings->first->billing_profile;
my $old_ratio = _get_free_ratio($contract_create,$old_start_mode,$balance->start,$balance->end);
my $old_free_cash = $old_ratio * ($profile->interval_free_cash // _DEFAULT_PROFILE_FREE_CASH);
my $old_free_time = $old_ratio * ($profile->interval_free_time // _DEFAULT_PROFILE_FREE_TIME);
my $new_ratio = _get_free_ratio($contract_create,$new_start_mode,$balance->start,$etime);
my $new_free_cash = $new_ratio * ($profile->interval_free_cash // _DEFAULT_PROFILE_FREE_CASH);
my $new_free_time = $new_ratio * ($profile->interval_free_time // _DEFAULT_PROFILE_FREE_TIME);
$cash_balance = $new_free_cash - $old_free_cash;
$free_time_balance += $new_free_time - $old_free_time;
}
return [cash_balance => sprintf("%.4f",$cash_balance), free_time_balance => sprintf("%.0f",$free_time_balance)];
}
sub _get_balance_values {
my %params = @_;
my($c, $profile, $last_profile, $contract, $last_balance, $stime, $etime, $initial_balance, $carry_over_mode, $now, $start_mode, $schema) = @params{qw/c profile last_profile contract last_balance stime etime initial_balance carry_over_mode now start_mode schema/};
$schema //= $c->model('DB');
$now //= $contract->create_timestamp // $contract->modify_timestamp;
my ($cash_balance,$cash_balance_interval, $free_time_balance, $free_time_balance_interval) = (0.0,0.0,0,0);
my $ratio;
if ($last_balance) {
if (_CARRY_OVER_MODE eq $carry_over_mode || (_CARRY_OVER_TIMELY_MODE eq $carry_over_mode && $last_balance->timely_count > 0)) {
#if (!defined $last_profile) {
# my $bm_last = get_actual_billing_mapping(schema => $schema, contract => $contract, now => $last_balance->start); #end); !?
# $last_profile = $bm_last->billing_mappings->first->billing_profile;
#}
my $contract_create = $contract->create_timestamp // $contract->modify_timestamp;
$ratio = 1.0;
if ($last_balance->start <= $contract_create && $last_balance->end >= $contract_create) {
$ratio = _get_free_ratio($contract_create,$start_mode,$last_balance->start,$last_balance->end);
}
my $old_free_cash = $ratio * ($last_profile->interval_free_cash // _DEFAULT_PROFILE_FREE_CASH);
$cash_balance = $last_balance->cash_balance;
if ($last_balance->cash_balance_interval < $old_free_cash) {
$cash_balance += $last_balance->cash_balance_interval - $old_free_cash;
}
#$ratio * $last_profile->interval_free_time // _DEFAULT_PROFILE_FREE_TIME
}
$ratio = 1.0;
} else {
$cash_balance = (defined $initial_balance ? $initial_balance : _DEFAULT_INITIAL_BALANCE);
$ratio = _get_free_ratio($now,$start_mode,$stime, $etime);
}
my $free_cash = $ratio * ($profile->interval_free_cash // _DEFAULT_PROFILE_FREE_CASH);
$cash_balance += $free_cash;
$cash_balance_interval = 0.0;
my $free_time = $ratio * ($profile->interval_free_time // _DEFAULT_PROFILE_FREE_TIME);
$free_time_balance = $free_time;
$free_time_balance_interval = 0;
return [cash_balance => sprintf("%.4f",$cash_balance),
cash_balance_interval => sprintf("%.4f",$cash_balance_interval),
free_time_balance => sprintf("%.0f",$free_time_balance),
free_time_balance_interval => sprintf("%.0f",$free_time_balance_interval)];
}
sub _get_free_ratio {
my ($now,$start_mode,$stime,$etime) = @_;
if (_TOPUP_START_MODE ne $start_mode) {
my $ctime;
if (defined $now) {
$ctime = ($now->clone->truncate(to => 'day') > $stime ? $now->clone->truncate(to => 'day') : $now);
} else {
$now = NGCP::Panel::Utils::DateTime::current_local;
$ctime = $now->clone->truncate(to => 'day') > $stime ? $now->truncate(to => 'day') : $now;
}
#my $ctime = (defined $now ? $now->clone : NGCP::Panel::Utils::DateTime::current_local);
#$ctime->truncate(to => 'day') if $ctime->clone->truncate(to => 'day') > $stime;
my $start_of_next_interval = $etime->clone->add(seconds => 1);
return ($start_of_next_interval->epoch - $ctime->epoch) / ($start_of_next_interval->epoch - $stime->epoch);
}
return 1.0;
}
sub _get_balance_interval_start_end {
my (%params) = @_;
my ($now,$start_mode,$last_etime,$interval_unit,$interval_value,$create) = @params{qw/now start_mode last_etime interval_unit interval_value create/};
my ($stime,$etime,$ctime) = (undef,undef,$now // NGCP::Panel::Utils::DateTime::current_local);
unless ($last_etime) {
$stime = _get_interval_start($ctime,$start_mode);
} else {
$stime = $last_etime->clone->add(seconds => 1);
}
if (defined $stime) { #lets crash in the create statement
if (_TOPUP_START_MODE ne $start_mode) {
$etime = _add_interval($stime,$interval_unit,$interval_value,_START_MODE_PRESERVE_EOM->{$start_mode} ? $create : undef)->subtract(seconds => 1);
} else {
$etime = NGCP::Panel::Utils::DateTime::infinite_future;
}
}
return ($stime,$etime);
}
sub _get_resized_interval_end {
my (%params) = @_;
my ($ctime, $create, $start_mode) = @params{qw/ctime create_timestamp start_mode/};
if (_CREATE_START_MODE eq $start_mode) {
my $start_of_next_interval;
if ($ctime->day >= $create->day) {
#e.g. ctime=30. Jan 2015 17:53, create=30. -> 28. Feb 2015 00:00
$start_of_next_interval = $ctime->clone->set(day => $create->day)->truncate(to => 'day')->add(months => 1, end_of_month => 'limit');
} else {
my $last_day_of_month = NGCP::Panel::Utils::DateTime::last_day_of_month($ctime);
if ($create->day > $last_day_of_month) {
#e.g. ctime=28. Feb 2015 17:53, create=30. -> 30. Mar 2015 00:00
$start_of_next_interval = $ctime->clone->add(months => 1)->set(day => $create->day)->truncate(to => 'day');
} else {
#e.g. ctime=15. Jul 2015 17:53, create=16. -> 16. Jul 2015 00:00
$start_of_next_interval = $ctime->clone->set(day => $create->day)->truncate(to => 'day');
}
}
return $start_of_next_interval->subtract(seconds => 1);
} elsif (_1ST_START_MODE eq $start_mode) {
return $ctime->clone->truncate(to => 'month')->add(months => 1)->subtract(seconds => 1);
} elsif (_TOPUP_START_MODE eq $start_mode) {
return $ctime->clone; #->add(seconds => 1);
#return NGCP::Panel::Utils::DateTime::infinite_future;
}
return undef;
}
sub _get_interval_start {
my ($ctime,$start_mode) = @_;
if (_CREATE_START_MODE eq $start_mode) {
return $ctime->clone->truncate(to => 'day');
} elsif (_1ST_START_MODE eq $start_mode) {
return $ctime->clone->truncate(to => 'month');
} elsif (_TOPUP_START_MODE eq $start_mode) {
return $ctime->clone; #->truncate(to => 'day');
}
return undef;
}
sub _add_interval {
my ($from,$interval_unit,$interval_value,$align_eom_dt) = @_;
if ('day' eq $interval_unit) {
return $from->clone->add(days => $interval_value);
} elsif ('week' eq $interval_unit) {
return $from->clone->add(weeks => $interval_value);
} elsif ('month' eq $interval_unit) {
my $to = $from->clone->add(months => $interval_value, end_of_month => 'preserve');
#'preserve' mode correction:
if (defined $align_eom_dt
&& $to->day > $align_eom_dt->day
&& $from->day == NGCP::Panel::Utils::DateTime::last_day_of_month($from)) {
my $delta = NGCP::Panel::Utils::DateTime::last_day_of_month($align_eom_dt) - $align_eom_dt->day;
$to->set(day => NGCP::Panel::Utils::DateTime::last_day_of_month($to) - $delta);
}
return $to;
}
return undef;
}
sub get_actual_billing_mapping {
my %params = @_;
my ($c,$schema,$contract,$now) = @params{qw/c schema contract now/};
$schema //= $c->model('DB');
$now //= NGCP::Panel::Utils::DateTime::current_local;
my $dtf = $schema->storage->datetime_parser;
return $schema->resultset('billing_mappings_actual')->search({ contract_id => $contract->id },{bind => [ ( $dtf->format_datetime($now) ) x 2],})->first;
}
sub check_balance_interval {
my (%params) = @_;
@ -24,6 +471,9 @@ sub check_balance_interval {
unless(defined $resource->{balance_interval_unit} && defined $resource->{balance_interval_value}){
return 0 unless &{$err_code}("Balance interval definition required.",'balance_interval');
}
unless($resource->{balance_interval_value} > 0) {
return 0 unless &{$err_code}("Balance interval has to be greater than 0 interval units.",'balance_interval');
}
return 1;
}
@ -36,10 +486,13 @@ sub check_carry_over_mode {
$err_code = sub { return 0; };
}
if (defined $resource->{carry_over_mode} && $resource->{carry_over_mode} eq _CARRY_OVER_TIMELY) {
if (defined $resource->{carry_over_mode} && $resource->{carry_over_mode} eq _CARRY_OVER_TIMELY_MODE) {
unless(defined $resource->{timely_duration_unit} && defined $resource->{timely_duration_value}){
return 0 unless &{$err_code}("'timely' interval definition required.",'timely_duration');
}
unless($resource->{balance_interval_value} > 0) {
return 0 unless &{$err_code}("'timely' interval has to be greater than 0 interval units.",'timely_duration');
}
}
return 1;
}

@ -25,6 +25,7 @@ around handle => sub {
my $stime = NGCP::Panel::Utils::DateTime::current_local->truncate(to => 'month');
my $etime = $stime->clone->add(months => 1);
#how to catchup contract balances of all contracts here?
$c->stash(
profiles => $c->model('DB')->resultset('billing_profiles')->search_rs({
status => { '!=' => 'terminated'},

@ -10,6 +10,11 @@ log4perl.appender.Default.layout=PatternLayout
log4perl.appender.Default.layout.ConversionPattern=%d{ISO8601} [%p] [%F +%L] %m{chomp}%n
# perhaps also add: host=%H pid=%P
<api_debug_opts>
allow_fake_client_time 1
allow_delay_commit 1
</api_debug_opts>
<Model::DB>
schema_class NGCP::Schema
</Model::DB>

@ -1 +1 @@
PERL5LIB=/opt/Komodo-IDE-9/remote_debugging PERLDB_OPTS="RemotePort=127.0.0.1:9000" DBGP_IDEKEY="jdoe" CATALYST_DEBUG=1 DBIC_TRACE=1 DBIC_TRACE_PROFILE=console DEVEL_CONFESS_OPTIONS='objects builtin dump color source' perl -d `which plackup` -I ../data-hal/lib -I ../ngcp-schema/lib -I lib -I ../sipwise-base/lib/ ngcp_panel.psgi --listen /tmp/ngcp_panel_sock --nproc 1 -s FCGI -r
PERL5LIB=/opt/Komodo-IDE-9/remote_debugging PERLDB_OPTS="RemotePort=127.0.0.1:9000" DBGP_IDEKEY="jdoe" CATALYST_DEBUG=1 DBIC_TRACE=1 DBIC_TRACE_PROFILE=console DEVEL_CONFESS_OPTIONS='objects builtin dump color source' perl -d `which plackup` -I ../data-hal/lib -I ../ngcp-schema/lib -I lib -I ../sipwise-base/lib/ ngcp_panel.psgi --listen /tmp/ngcp_panel_sock --nproc 2 -s FCGI -r

@ -0,0 +1,645 @@
use strict;
use warnings;
use threads qw();
use threads::shared qw();
#use Sipwise::Base; #causes segfault when creating threads..
use Net::Domain qw(hostfqdn);
use LWP::UserAgent;
use JSON qw();
use Test::More;
#use Storable qw();
use Time::Fake;
use DateTime::Format::Strptime;
use DateTime::Format::ISO8601;
use JSON::PP;
use LWP::Debug;
BEGIN {
unshift(@INC,'../lib');
}
use NGCP::Panel::Utils::DateTime qw();
my $is_local_env = 1;
use Config::General;
my $catalyst_config;
if ($is_local_env) {
$catalyst_config = Config::General->new("../ngcp_panel.conf");
} else {
#taken 1:1 from /lib/NGCP/Panel.pm
my $panel_config;
for my $path(qw#/etc/ngcp-panel/ngcp_panel.conf etc/ngcp_panel.conf ngcp_panel.conf#) {
if(-f $path) {
$panel_config = $path;
last;
}
}
$panel_config //= 'ngcp_panel.conf';
$catalyst_config = Config::General->new($panel_config);
}
my %config = $catalyst_config->getall();
my $uri = $ENV{CATALYST_SERVER} || ('https://'.hostfqdn.':4443');
my $valid_ssl_client_cert = $ENV{API_SSL_CLIENT_CERT} ||
"/etc/ngcp-panel/api_ssl/NGCP-API-client-certificate.pem";
my $valid_ssl_client_key = $ENV{API_SSL_CLIENT_KEY} ||
$valid_ssl_client_cert;
my $ssl_ca_cert = $ENV{API_SSL_CA_CERT} || "/etc/ngcp-panel/api_ssl/api_ca.crt";
my ($ua, $req, $res);
$ua = LWP::UserAgent->new;
if ($is_local_env) {
$ua->ssl_opts(
verify_hostname => 0,
);
$ua->credentials("127.0.0.1:4443", "api_admin_http", 'administrator', 'administrator');
#$ua->timeout(500); #useless, need to change the nginx timeout
} else {
$ua->ssl_opts(
SSL_cert_file => $valid_ssl_client_cert,
SSL_key_file => $valid_ssl_client_key,
SSL_ca_file => $ssl_ca_cert,
);
}
{
my $future = NGCP::Panel::Utils::DateTime::infinite_future;
my $past = NGCP::Panel::Utils::DateTime::infinite_past;
my $now = NGCP::Panel::Utils::DateTime::current_local;
my $dtf = DateTime::Format::Strptime->new(
pattern => '%F %T',
);
is($dtf->format_datetime($future),'9999-12-31 23:59:59','check if infinite future is 9999-12-31 23:59:59');
is($dtf->format_datetime($past),'1000-01-01 00:00:00','check if infinite past is 1000-01-01 00:00:00');
foreach my $offset ((0,'+'. 80*365*24*60*60 .'s','-'. 80*365*24*60*60 .'s')) {
my ($fake_now,$offset_label);
if ($offset) {
_set_time($offset);
$fake_now = NGCP::Panel::Utils::DateTime::current_local;
my $delta = $fake_now->epoch - $now->epoch;
my $delta_offset = substr($offset,0,length($offset)-2) * 1;
ok(abs($delta) > abs($delta_offset) && abs($delta_offset) > 0,"'Great Scott!'");
ok($delta > $delta_offset,'check fake time offset of ' . $offset . ': ' . $delta) if $delta_offset > 0;
ok(-1 * $delta > -1 * $delta_offset,'check fake time offset of ' . $offset . ': ' . $delta) if $delta_offset < 0;
$offset_label = 'fake time offset: ' . $offset . ': ';
} else {
$fake_now = $now;
$offset_label = '';
}
ok($future > $fake_now,$offset_label . 'future is greater than now');
ok(!($future < $fake_now),$offset_label . 'future is not smaller than now');
ok($past < $fake_now,$offset_label . 'past is smaller than now');
ok(!($past > $fake_now),$offset_label . 'past is not greater than now');
ok($future->epoch > $fake_now->epoch,$offset_label . 'future is greater than now (epoch)');
ok(!($future->epoch < $fake_now->epoch),$offset_label . 'future is not smaller than now (epoch)');
ok($past->epoch < $fake_now->epoch,$offset_label . 'past is smaller than now (epoch)');
ok(!($past->epoch > $fake_now->epoch),$offset_label . 'past is not greater than now (epoch)');
}
#use DateTime::Infinite;
#$past = DateTime::Infinite::Past->new();
#$future = DateTime::Infinite::Future->new();
_set_time();
}
my $t = time;
my $default_reseller_id = 1;
$req = HTTP::Request->new('POST', $uri.'/api/billingprofiles/');
$req->header('Content-Type' => 'application/json');
$req->header('Prefer' => 'return=representation');
$req->content(JSON::to_json({
name => "test profile $t",
handle => "testprofile$t",
reseller_id => $default_reseller_id,
}));
$res = $ua->request($req);
is($res->code, 201, "POST test billing profile");
my $billingprofile_uri = $uri.'/'.$res->header('Location');
$req = HTTP::Request->new('GET', $billingprofile_uri);
$res = $ua->request($req);
is($res->code, 200, "fetch POSTed billing profile");
my $billingprofile = JSON::from_json($res->decoded_content);
# first, create a contact
$req = HTTP::Request->new('POST', $uri.'/api/customercontacts/');
$req->header('Content-Type' => 'application/json');
$req->content(JSON::to_json({
firstname => "cust_contact_first",
lastname => "cust_contact_last",
email => "cust_contact\@custcontact.invalid",
reseller_id => $default_reseller_id,
}));
$res = $ua->request($req);
is($res->code, 201, "create customer contact");
$req = HTTP::Request->new('GET', $uri.'/'.$res->header('Location'));
$res = $ua->request($req);
is($res->code, 200, "fetch customer contact");
my $custcontact = JSON::from_json($res->decoded_content);
my %customer_map :shared = ();
my $profile_map = {};
if (_get_allow_fake_client_time()) {
my $prof_package_create30d = _create_profile_package('create','day',30);
my $prof_package_1st30d = _create_profile_package('1st','day',30);
my $prof_package_create1m = _create_profile_package('create','month',1);
my $prof_package_1st1m = _create_profile_package('1st','month',1);
my $prof_package_create2w = _create_profile_package('create','week',2);
my $prof_package_1st2w = _create_profile_package('1st','week',2);
my $prof_package_topup = _create_profile_package('topup');
{
_set_time(NGCP::Panel::Utils::DateTime::from_string('2014-12-30 13:00:00'));
my $customer_wo = _create_customer();
my $customer_create1m = _create_customer($prof_package_create1m);
_set_time(NGCP::Panel::Utils::DateTime::from_string('2015-04-02 02:00:00'));
_check_interval_history($customer_wo,[
{ start => '2014-12-01 00:00:00', stop => '2014-12-31 23:59:59'},
{ start => '2015-01-01 00:00:00', stop => '2015-01-31 23:59:59'},
{ start => '2015-02-01 00:00:00', stop => '2015-02-28 23:59:59'},
{ start => '2015-03-01 00:00:00', stop => '2015-03-31 23:59:59'},
{ start => '2015-04-01 00:00:00', stop => '2015-04-30 23:59:59'},
]); #,NGCP::Panel::Utils::DateTime::from_string('2014-11-29 13:00:00'));
_check_interval_history($customer_create1m,[
{ start => '2014-12-30 00:00:00', stop => '2015-01-29 23:59:59'},
{ start => '2015-01-30 00:00:00', stop => '2015-02-27 23:59:59'},
{ start => '2015-02-28 00:00:00', stop => '2015-03-29 23:59:59'},
{ start => '2015-03-30 00:00:00', stop => '2015-04-29 23:59:59'},
]);
_set_time();
}
{
my $ts = '2014-01-07 13:00:00';
_set_time(NGCP::Panel::Utils::DateTime::from_string($ts));
my $customer = _create_customer();
_check_interval_history($customer,[
{ start => '2014-01-01 00:00:00', stop => '2014-01-31 23:59:59'},
]);
$ts = '2014-03-01 13:00:00';
_set_time(NGCP::Panel::Utils::DateTime::from_string($ts));
_check_interval_history($customer,[
{ start => '2014-01-01 00:00:00', stop => '2014-01-31 23:59:59'},
{ start => '2014-02-01 00:00:00', stop => '2014-02-28 23:59:59'},
{ start => '2014-03-01 00:00:00', stop => '2014-03-31 23:59:59'},
]);
_switch_package($customer,$prof_package_create30d);
_check_interval_history($customer,[
{ start => '2014-01-01 00:00:00', stop => '2014-01-31 23:59:59'},
{ start => '2014-02-01 00:00:00', stop => '2014-02-28 23:59:59'},
{ start => '2014-03-01 00:00:00', stop => '2014-03-06 23:59:59'},
]);
$ts = '2014-04-01 13:00:00';
_set_time(NGCP::Panel::Utils::DateTime::from_string($ts));
_check_interval_history($customer,[
{ start => '2014-01-01 00:00:00', stop => '2014-01-31 23:59:59'},
{ start => '2014-02-01 00:00:00', stop => '2014-02-28 23:59:59'},
{ start => '2014-03-01 00:00:00', stop => '2014-03-06 23:59:59'},
{ start => '2014-03-07 00:00:00', stop => '2014-04-05 23:59:59'},
]);
_switch_package($customer,$prof_package_1st30d);
_check_interval_history($customer,[
{ start => '2014-01-01 00:00:00', stop => '2014-01-31 23:59:59'},
{ start => '2014-02-01 00:00:00', stop => '2014-02-28 23:59:59'},
{ start => '2014-03-01 00:00:00', stop => '2014-03-06 23:59:59'},
{ start => '2014-03-07 00:00:00', stop => '2014-04-30 23:59:59'},
]);
$ts = '2014-05-13 13:00:00';
_set_time(NGCP::Panel::Utils::DateTime::from_string($ts));
_check_interval_history($customer,[
{ start => '2014-01-01 00:00:00', stop => '2014-01-31 23:59:59'},
{ start => '2014-02-01 00:00:00', stop => '2014-02-28 23:59:59'},
{ start => '2014-03-01 00:00:00', stop => '2014-03-06 23:59:59'},
{ start => '2014-03-07 00:00:00', stop => '2014-04-30 23:59:59'},
{ start => '2014-05-01 00:00:00', stop => '2014-05-30 23:59:59'},
]);
_switch_package($customer,$prof_package_create1m);
_check_interval_history($customer,[
{ start => '2014-01-01 00:00:00', stop => '2014-01-31 23:59:59'},
{ start => '2014-02-01 00:00:00', stop => '2014-02-28 23:59:59'},
{ start => '2014-03-01 00:00:00', stop => '2014-03-06 23:59:59'},
{ start => '2014-03-07 00:00:00', stop => '2014-04-30 23:59:59'},
{ start => '2014-05-01 00:00:00', stop => '2014-06-06 23:59:59'},
]);
$ts = '2014-05-27 13:00:00';
_set_time(NGCP::Panel::Utils::DateTime::from_string($ts));
_check_interval_history($customer,[
{ start => '2014-01-01 00:00:00', stop => '2014-01-31 23:59:59'},
{ start => '2014-02-01 00:00:00', stop => '2014-02-28 23:59:59'},
{ start => '2014-03-01 00:00:00', stop => '2014-03-06 23:59:59'},
{ start => '2014-03-07 00:00:00', stop => '2014-04-30 23:59:59'},
{ start => '2014-05-01 00:00:00', stop => '2014-06-06 23:59:59'},
]);
_switch_package($customer,$prof_package_1st1m);
_check_interval_history($customer,[
{ start => '2014-01-01 00:00:00', stop => '2014-01-31 23:59:59'},
{ start => '2014-02-01 00:00:00', stop => '2014-02-28 23:59:59'},
{ start => '2014-03-01 00:00:00', stop => '2014-03-06 23:59:59'},
{ start => '2014-03-07 00:00:00', stop => '2014-04-30 23:59:59'},
{ start => '2014-05-01 00:00:00', stop => '2014-05-31 23:59:59'},
]);
my $t1 = $ts;
$ts = '2014-08-03 13:00:00';
_set_time(NGCP::Panel::Utils::DateTime::from_string($ts));
_switch_package($customer,$prof_package_create2w);
_check_interval_history($customer,[
{ start => '2014-06-01 00:00:00', stop => '2014-06-30 23:59:59'},
{ start => '2014-07-01 00:00:00', stop => '2014-07-31 23:59:59'},
{ start => '2014-08-01 00:00:00', stop => '2014-08-06 23:59:59'},
],NGCP::Panel::Utils::DateTime::from_string($t1));
$t1 = $ts;
$ts = '2014-09-03 13:00:00';
_set_time(NGCP::Panel::Utils::DateTime::from_string($ts));
_switch_package($customer,$prof_package_1st2w);
_check_interval_history($customer,[
{ start => '2014-08-07 00:00:00', stop => '2014-08-20 23:59:59'},
{ start => '2014-08-21 00:00:00', stop => '2014-09-30 23:59:59'},
],NGCP::Panel::Utils::DateTime::from_string($t1));
#$t1 = $ts;
#$ts = '2014-09-03 13:00:00';
#_set_time(NGCP::Panel::Utils::DateTime::from_string($ts));
_switch_package($customer);
_check_interval_history($customer,[
{ start => '2014-08-07 00:00:00', stop => '2014-08-20 23:59:59'},
{ start => '2014-08-21 00:00:00', stop => '2014-09-30 23:59:59'},
],NGCP::Panel::Utils::DateTime::from_string($t1));
_set_time();
}
if (_get_allow_delay_commit()) {
_set_time(NGCP::Panel::Utils::DateTime::current_local->subtract(months => 3));
_create_customers_threaded(3);
_set_time();
my $t1 = time;
#_fetch_intervals_worker(0,'asc');
#_fetch_intervals_worker(0,'desc');
my $delay = 2;
my $t_a = threads->create(\&_fetch_intervals_worker,$delay,'id','asc');
my $t_b = threads->create(\&_fetch_intervals_worker,$delay,'id','desc');
my $intervals_a = $t_a->join();
my $intervals_b = $t_b->join();
my $t2 = time;
is_deeply([ sort { $a->{id} cmp $b->{id} } @{ $intervals_b->{_embedded}->{'ngcp:balanceintervals'} } ],$intervals_a->{_embedded}->{'ngcp:balanceintervals'},'compare interval collection results of threaded requests deeply');
ok($t2 - $t1 > 2*$delay,'expected delay to assume requests were processed after another');
} else {
diag('allow_delay_commit not set, skipping ...');
}
} else {
diag('allow_fake_client_time not set, skipping ...');
}
{ #test balanceintervals root collection and item
_create_customers_threaded(3) unless _get_allow_fake_client_time();
my $total_count = (scalar keys %customer_map);
my $nexturi = $uri.'/api/balanceintervals/?page=1&rows=' . ((not defined $total_count or $total_count <= 2) ? 2 : $total_count - 1) . '&contact_id='.$custcontact->{id};
do {
$req = HTTP::Request->new('GET',$nexturi);
$req->header('X-Fake-Clienttime' => _get_rfc_1123_now());
$res = $ua->request($req);
#$res = $ua->get($nexturi);
is($res->code, 200, "balanceintervals root collection: fetch balance intervals collection page");
my $collection = JSON::from_json($res->decoded_content);
my $selfuri = $uri . $collection->{_links}->{self}->{href};
is($selfuri, $nexturi, "balanceintervals root collection: check _links.self.href of collection");
my $colluri = URI->new($selfuri);
ok(defined $total_count ? ($collection->{total_count} == $total_count) : ($collection->{total_count} > 0), "balanceintervals root collection: check 'total_count' of collection");
my %q = $colluri->query_form;
ok(exists $q{page} && exists $q{rows}, "balanceintervals root collection: check existence of 'page' and 'row' in 'self'");
my $page = int($q{page});
my $rows = int($q{rows});
if($page == 1) {
ok(!exists $collection->{_links}->{prev}->{href}, "balanceintervals root collection: check absence of 'prev' on first page");
} else {
ok(exists $collection->{_links}->{prev}->{href}, "balanceintervals root collection: check existence of 'prev'");
}
if(($collection->{total_count} / $rows) <= $page) {
ok(!exists $collection->{_links}->{next}->{href}, "balanceintervals root collection: check absence of 'next' on last page");
} else {
ok(exists $collection->{_links}->{next}->{href}, "balanceintervals root collection: check existence of 'next'");
}
if($collection->{_links}->{next}->{href}) {
$nexturi = $uri . $collection->{_links}->{next}->{href};
} else {
$nexturi = undef;
}
# TODO: I'd expect that to be an array ref in any case!
ok(ref $collection->{_links}->{'ngcp:balanceintervals'} eq "ARRAY", "balanceintervals root collection: check if 'ngcp:balanceintervals' is array");
my $page_items = {};
foreach my $interval_link (@{ $collection->{_links}->{'ngcp:balanceintervals'} }) {
#delete $customers{$c->{href}};
#ok(exists $journals->{$journal->{href}},"check page journal item link");
$req = HTTP::Request->new('GET',$uri . $interval_link->{href});
$req->header('X-Fake-Clienttime' => _get_rfc_1123_now());
$res = $ua->request($req);
is($res->code, 200, "balanceintervals root collection: fetch page balance interval item");
my $interval = JSON::from_json($res->decoded_content);
$page_items->{$interval->{id}} = $interval;
}
foreach my $interval (@{ $collection->{_embedded}->{'ngcp:balanceintervals'} }) {
ok(exists $page_items->{$interval->{id}},"balanceintervals root collection: check existence of linked item among embedded");
my $fetched = delete $page_items->{$interval->{id}};
delete $fetched->{content};
is_deeply($interval,$fetched,"balanceintervals root collection: compare fetched and embedded item deeply");
}
ok((scalar keys $page_items) == 0,"balanceintervals root collection: check if all embedded items are linked");
} while($nexturi);
}
done_testing;
sub _check_interval_history {
my ($customer,$expected_interval_history,$limit_dt) = @_;
my $total_count = (scalar @$expected_interval_history);
#my @got_interval_history = ();
my $i = 0;
my $limit = '';
$limit = '&start=' . DateTime::Format::ISO8601->parse_datetime($limit_dt) if defined $limit_dt;
my $label = 'interval history of contract with ' . ($customer->{profile_package_id} ? 'package ' . $profile_map->{$customer->{profile_package_id}}->{name} : 'no package') . ': ';
my $nexturi = $uri.'/api/balanceintervals/'.$customer->{id}.'/?page=1&rows=10&order_by_direction=asc&order_by=start'.$limit;
do {
$req = HTTP::Request->new('GET',$nexturi);
$req->header('X-Fake-Clienttime' => _get_rfc_1123_now());
$res = $ua->request($req);
#$res = $ua->get($nexturi);
is($res->code, 200, $label . "fetch balance intervals collection page");
my $collection = JSON::from_json($res->decoded_content);
my $selfuri = $uri . $collection->{_links}->{self}->{href};
is($selfuri, $nexturi, $label . "check _links.self.href of collection");
my $colluri = URI->new($selfuri);
ok($collection->{total_count} == $total_count, $label . "check 'total_count' of collection");
my %q = $colluri->query_form;
ok(exists $q{page} && exists $q{rows}, $label . "check existence of 'page' and 'row' in 'self'");
my $page = int($q{page});
my $rows = int($q{rows});
if($page == 1) {
ok(!exists $collection->{_links}->{prev}->{href}, $label . "check absence of 'prev' on first page");
} else {
ok(exists $collection->{_links}->{prev}->{href}, $label . "check existence of 'prev'");
}
if(($collection->{total_count} / $rows) <= $page) {
ok(!exists $collection->{_links}->{next}->{href}, $label . "check absence of 'next' on last page");
} else {
ok(exists $collection->{_links}->{next}->{href}, $label . "check existence of 'next'");
}
if($collection->{_links}->{next}->{href}) {
$nexturi = $uri . $collection->{_links}->{next}->{href};
} else {
$nexturi = undef;
}
# TODO: I'd expect that to be an array ref in any case!
ok(ref $collection->{_links}->{'ngcp:balanceintervals'} eq "ARRAY", $label . "check if 'ngcp:balanceintervals' is array");
my $page_items = {};
#foreach my $interval_link (@{ $collection->{_links}->{'ngcp:balanceintervals'} }) {
# #delete $customers{$c->{href}};
# #ok(exists $journals->{$journal->{href}},"check page journal item link");
#
# $req = HTTP::Request->new('GET',$uri . $interval_link->{href});
# $req->header('X-Fake-Clienttime' => _get_rfc_1123_now());
# $res = $ua->request($req);
# is($res->code, 200, $label . "fetch page balance interval item");
# my $interval = JSON::from_json($res->decoded_content);
#
# $page_items->{$interval->{id}} = $interval;
#}
foreach my $interval (@{ $collection->{_embedded}->{'ngcp:balanceintervals'} }) {
#ok(exists $page_items->{$interval->{id}},$label . "check existence of linked item among embedded");
#my $fetched = delete $page_items->{$interval->{id}};
#delete $fetched->{content};
#is_deeply($interval,$fetched,$label . "compare fetched and embedded item deeply");
_compare_interval($interval,$expected_interval_history->[$i],$label);
$i++
}
#ok((scalar keys $page_items) == 0,$label . "check if all embedded items are linked");
} while($nexturi);
ok($i == $total_count,$label . "check if all expected items are listed");
}
sub _compare_interval {
my ($got,$expected,$label) = @_;
if ($expected->{start}) {
is(NGCP::Panel::Utils::DateTime::from_string($got->{start}),NGCP::Panel::Utils::DateTime::from_string($expected->{start}),$label . "check interval " . $got->{id} . " start timestmp");
}
if ($expected->{stop}) {
is(NGCP::Panel::Utils::DateTime::from_string($got->{stop}),NGCP::Panel::Utils::DateTime::from_string($expected->{stop}),$label . "check interval " . $got->{id} . " stop timestmp");
}
}
sub _fetch_intervals_worker {
my ($delay,$sort_column,$dir) = @_;
diag("starting thread " . threads->tid() . " ...");
$req = HTTP::Request->new('GET', $uri.'/api/balanceintervals/?order_by='.$sort_column.'&order_by_direction='.$dir.'&contact_id='.$custcontact->{id}.'&rows='.(scalar keys %customer_map));
$req->header('X-Fake-Clienttime' => _get_rfc_1123_now());
$req->header('X-Delay-Commit' => $delay);
$res = $ua->request($req);
is($res->code, 200, "thread " . threads->tid() . ": concurrent fetch balanceintervals of " . (scalar keys %customer_map) . " contracts of contact id ".$custcontact->{id} . " in " . $dir . " order");
my $result = JSON::from_json($res->decoded_content);
diag("finishing thread " . threads->tid() . " ...");
return $result;
}
sub _create_customers_threaded {
my ($number_of_customers) = @_;
my $t0 = time;
my @t_cs = ();
#my $number_of_customers = 3;
for (1..$number_of_customers) {
my $t_c = threads->create(\&_create_customer);
push(@t_cs,$t_c);
}
foreach my $t_c (@t_cs) {
$t_c->join();
}
my $t1 = time;
diag('average time to create a customer: ' . ($t1 - $t0)/$number_of_customers);
}
sub _create_customer {
my ($package) = @_;
$req = HTTP::Request->new('POST', $uri.'/api/customers/');
$req->header('Content-Type' => 'application/json');
$req->header('X-Fake-Clienttime' => _get_rfc_1123_now());
$req->content(JSON::to_json({
status => "active",
contact_id => $custcontact->{id},
type => "sipaccount",
($package ? (billing_profile_definition => 'package',
profile_package_id => $package->{id}) :
(billing_profile_id => $billingprofile->{id})),
max_subscribers => undef,
external_id => undef,
}));
$res = $ua->request($req);
my $label = 'test customer ' . ($package ? 'with package ' . $package->{name} : 'w/o profile package');
is($res->code, 201, "create " . $label);
$req = HTTP::Request->new('GET', $uri.'/'.$res->header('Location'));
$req->header('X-Fake-Clienttime' => _get_rfc_1123_now());
$res = $ua->request($req);
is($res->code, 200, "fetch " . $label);
my $customer = JSON::from_json($res->decoded_content);
$customer_map{$customer->{id}} = threads::shared::shared_clone($customer);
return $customer;
}
sub _switch_package {
my ($customer,$package) = @_;
$req = HTTP::Request->new('PATCH', $uri.'/api/customers/'.$customer->{id});
$req->header('Prefer' => 'return=representation');
$req->header('Content-Type' => 'application/json-patch+json');
$req->header('X-Fake-Clienttime' => _get_rfc_1123_now());
$req->content(JSON::to_json(
[ { op => 'replace', path => '/profile_package_id', value => ($package ? $package->{id} : undef) } ]
));
$res = $ua->request($req);
is($res->code, 200, "patch customer from " . ($customer->{profile_package_id} ? 'package ' . $profile_map->{$customer->{profile_package_id}}->{name} : 'no package') . " to " .
($package ? $package->{name} : 'no package'));
return JSON::from_json($res->decoded_content);
}
sub _set_time {
my ($o) = @_;
my $dtf = DateTime::Format::Strptime->new(
pattern => '%F %T',
);
if (defined $o) {
$o = $o->epoch if ref $o eq 'DateTime';
Time::Fake->offset($o);
my $now = NGCP::Panel::Utils::DateTime::current_local;
diag("applying fake time offset '$o' - current time: " . $dtf->format_datetime($now));
} else {
Time::Fake->reset();
my $now = NGCP::Panel::Utils::DateTime::current_local;
diag("resetting fake time - current time: " . $dtf->format_datetime($now));
}
}
sub _get_rfc_1123_now {
return NGCP::Panel::Utils::DateTime::to_rfc1123_string(NGCP::Panel::Utils::DateTime::current_local);
}
sub _create_profile_package {
my ($start_mode,$interval_unit,$interval_value) = @_;
$req = HTTP::Request->new('POST', $uri.'/api/profilepackages/');
$req->header('Content-Type' => 'application/json');
$req->header('Prefer' => 'return=representation');
$req->header('X-Fake-Clienttime' => _get_rfc_1123_now());
my $name = $start_mode . ($interval_unit ? '/' . $interval_value . ' ' . $interval_unit : '');
$req->content(JSON::to_json({
name => "test '" . $name . "' profile package " . $t,
description => "test profile package description " . $t,
reseller_id => $default_reseller_id,
initial_profiles => [{ profile_id => $billingprofile->{id}, }, ],
balance_interval_start_mode => $start_mode,
($interval_unit ? (balance_interval_value => $interval_value,
balance_interval_unit => $interval_unit,) : ()),
}));
$res = $ua->request($req);
is($res->code, 201, "POST test profilepackage - '" . $name . "'");
my $profilepackage_uri = $uri.'/'.$res->header('Location');
$req = HTTP::Request->new('GET', $profilepackage_uri);
$req->header('X-Fake-Clienttime' => _get_rfc_1123_now());
$res = $ua->request($req);
is($res->code, 200, "fetch POSTed profilepackage - '" . $name . "'");
my $package = JSON::from_json($res->decoded_content);
$profile_map->{$package->{id}} = $package;
return $package;
}
sub _get_allow_delay_commit {
my $allow_delay_commit = 0;
my $cfg = $config{api_debug_opts};
$allow_delay_commit = ((defined $cfg->{allow_delay_commit}) && $cfg->{allow_delay_commit} ? 1 : 0) if defined $cfg;
return $allow_delay_commit;
}
sub _get_allow_fake_client_time {
my $allow_fake_client_time = 0;
my $cfg = $config{api_debug_opts};
$allow_fake_client_time = ((defined $cfg->{allow_fake_client_time}) && $cfg->{allow_fake_client_time} ? 1 : 0) if defined $cfg;
return $allow_fake_client_time;
}

@ -10,7 +10,7 @@ use DateTime qw();
use DateTime::Format::Strptime qw();
use DateTime::Format::ISO8601 qw();
my $is_local_env = 0;
my $is_local_env = 1;
my $uri = $ENV{CATALYST_SERVER} || ('https://'.hostfqdn.':4443');

@ -2,8 +2,6 @@
use strict;
#use Moose;
use Sipwise::Base;
use Test::Collection;
use Test::FakeData;
use Net::Domain qw(hostfqdn);
use LWP::UserAgent;
use HTTP::Request::Common;
@ -13,13 +11,21 @@ use Data::Dumper;
use File::Basename;
use bignum qw/hex/;
BEGIN {
unshift(@INC,'../t/lib');
}
use Test::Collection;
use Test::FakeData;
#init test_machine
my $test_machine = Test::Collection->new(
name => 'pbxdevices',
embedded => [qw/pbxdeviceprofiles customers/]
embedded => [qw/pbxdeviceprofiles customers/],
use_cert_login => 0,
);
$test_machine->methods->{collection}->{allowed} = {map {$_ => 1} qw(GET HEAD OPTIONS POST)};
$test_machine->methods->{item}->{allowed} = {map {$_ => 1} qw(GET HEAD OPTIONS PUT PATCH DELETE)};
my $fake_data = Test::FakeData->new;
$fake_data->set_data_from_script({
'pbxdevices' => {
@ -46,6 +52,7 @@ $fake_data->set_data_from_script({
'query' => ['station_name'],
},
});
#for item creation test purposes /post request data/
$test_machine->DATA_ITEM_STORE($fake_data->process('pbxdevices'));

@ -0,0 +1,28 @@
use threads qw();
#use Sipwise::Base;
use Test::More;
BEGIN {
unshift(@INC,'../lib');
}
use NGCP::Panel::Utils::DateTime qw();
my $delay = 5;
my $t_a = threads->create(sub {
diag('thread ' . threads->tid());
sleep($delay);
});
my $t_b = threads->create(sub {
diag('thread ' . threads->tid());
sleep($delay);
});
$t_a->join();
$t_b->join();
#ok($t_a + $t_b == 2,'test threads joined');
done_testing;

@ -31,10 +31,16 @@ has 'panel_config' => (
);
has 'ua' => (
is => 'rw',
lazy => 1,
isa => 'LWP::UserAgent',
lazy => 1,
builder => 'init_ua',
);
has 'use_cert_login' => (
is => 'rw',
isa => 'Bool',
default => 1,
);
has 'base_uri' => (
is => 'ro',
isa => 'Str',

Loading…
Cancel
Save