+ 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: I7bb54a83b76e510b1baa573a986d05400a7fec1echanges/73/2073/5
parent
0316e08a07
commit
c378681a24
@ -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:
|
@ -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:
|
@ -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:
|
@ -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;
|
||||
}
|
@ -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;
|
Loading…
Reference in new issue