From c378681a2429c17530d6e2ea62fe8fc20885f4cf Mon Sep 17 00:00:00 2001 From: Rene Krenn Date: Thu, 9 Jul 2015 09:39:10 +0200 Subject: [PATCH] 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 --- debian/control | 1 + lib/NGCP/Panel.pm | 2 +- .../Panel/Controller/API/BalanceIntervals.pm | 189 +++++ .../Controller/API/BalanceIntervalsItem.pm | 256 +++++++ lib/NGCP/Panel/Controller/API/Contracts.pm | 48 +- .../Panel/Controller/API/ContractsItem.pm | 23 +- .../Panel/Controller/API/CustomerBalances.pm | 30 +- .../Controller/API/CustomerBalancesItem.pm | 7 + lib/NGCP/Panel/Controller/API/Customers.pm | 28 +- .../Panel/Controller/API/CustomersItem.pm | 23 +- lib/NGCP/Panel/Controller/API/Root.pm | 2 +- .../Panel/Controller/API/TopupVouchers.pm | 2 +- lib/NGCP/Panel/Controller/Contract.pm | 47 +- lib/NGCP/Panel/Controller/Customer.pm | 92 ++- lib/NGCP/Panel/Controller/Invoice.pm | 62 +- lib/NGCP/Panel/Controller/Reseller.pm | 11 +- lib/NGCP/Panel/Controller/Root.pm | 31 +- lib/NGCP/Panel/Field/ContractBalance.pm | 19 + .../Panel/Form/Balance/BalanceIntervalAPI.pm | 93 +++ .../Form/{ => Balance}/CustomerBalance.pm | 4 +- lib/NGCP/Panel/Form/Invoice/Invoice.pm | 10 + lib/NGCP/Panel/Model/DB.pm | 11 + lib/NGCP/Panel/Role/API.pm | 45 +- lib/NGCP/Panel/Role/API/BalanceIntervals.pm | 154 +++++ lib/NGCP/Panel/Role/API/Contracts.pm | 82 ++- lib/NGCP/Panel/Role/API/CustomerBalances.pm | 60 +- lib/NGCP/Panel/Role/API/Customers.pm | 83 ++- lib/NGCP/Panel/Utils/Contract.pm | 201 +++--- lib/NGCP/Panel/Utils/DateTime.pm | 72 +- lib/NGCP/Panel/Utils/Form.pm | 2 +- lib/NGCP/Panel/Utils/Journal.pm | 4 +- lib/NGCP/Panel/Utils/Message.pm | 1 + lib/NGCP/Panel/Utils/ProfilePackages.pm | 457 ++++++++++++- .../Widget/Plugin/AdminBillingOverview.pm | 1 + ngcp_panel.conf | 5 + script/ngcp_panel_debug.sh | 2 +- t/api-balanceintervals.t | 645 ++++++++++++++++++ t/api-customers.t | 2 +- t/api-pbxdevices.t | 13 +- t/api-threads.t | 28 + t/lib/Test/Collection.pm | 6 + 41 files changed, 2527 insertions(+), 327 deletions(-) create mode 100644 lib/NGCP/Panel/Controller/API/BalanceIntervals.pm create mode 100644 lib/NGCP/Panel/Controller/API/BalanceIntervalsItem.pm create mode 100644 lib/NGCP/Panel/Field/ContractBalance.pm create mode 100644 lib/NGCP/Panel/Form/Balance/BalanceIntervalAPI.pm rename lib/NGCP/Panel/Form/{ => Balance}/CustomerBalance.pm (94%) create mode 100644 lib/NGCP/Panel/Role/API/BalanceIntervals.pm create mode 100644 t/api-balanceintervals.t create mode 100644 t/api-threads.t diff --git a/debian/control b/debian/control index 3ed92d3906..2ad3279835 100644 --- a/debian/control +++ b/debian/control @@ -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, diff --git a/lib/NGCP/Panel.pm b/lib/NGCP/Panel.pm index a152fb8fe9..ea3a04363e 100644 --- a/lib/NGCP/Panel.pm +++ b/lib/NGCP/Panel.pm @@ -1,6 +1,6 @@ package NGCP::Panel; -use Moose; +use Moose; use Catalyst::Runtime 5.80; # Set flags and add plugins for the application. diff --git a/lib/NGCP/Panel/Controller/API/BalanceIntervals.pm b/lib/NGCP/Panel/Controller/API/BalanceIntervals.pm new file mode 100644 index 0000000000..f3506d2165 --- /dev/null +++ b/lib/NGCP/Panel/Controller/API/BalanceIntervals.pm @@ -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: diff --git a/lib/NGCP/Panel/Controller/API/BalanceIntervalsItem.pm b/lib/NGCP/Panel/Controller/API/BalanceIntervalsItem.pm new file mode 100644 index 0000000000..84833fd212 --- /dev/null +++ b/lib/NGCP/Panel/Controller/API/BalanceIntervalsItem.pm @@ -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: diff --git a/lib/NGCP/Panel/Controller/API/Contracts.pm b/lib/NGCP/Panel/Controller/API/Contracts.pm index 8f314e3b04..6d25713236 100644 --- a/lib/NGCP/Panel/Controller/API/Contracts.pm +++ b/lib/NGCP/Panel/Controller/API/Contracts.pm @@ -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; } diff --git a/lib/NGCP/Panel/Controller/API/ContractsItem.pm b/lib/NGCP/Panel/Controller/API/ContractsItem.pm index 43e1632a3f..338fd51412 100644 --- a/lib/NGCP/Panel/Controller/API/ContractsItem.pm +++ b/lib/NGCP/Panel/Controller/API/ContractsItem.pm @@ -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; } diff --git a/lib/NGCP/Panel/Controller/API/CustomerBalances.pm b/lib/NGCP/Panel/Controller/API/CustomerBalances.pm index 44e09e3c0a..f3f9c36691 100644 --- a/lib/NGCP/Panel/Controller/API/CustomerBalances.pm +++ b/lib/NGCP/Panel/Controller/API/CustomerBalances.pm @@ -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); } diff --git a/lib/NGCP/Panel/Controller/API/CustomerBalancesItem.pm b/lib/NGCP/Panel/Controller/API/CustomerBalancesItem.pm index f41ca17a74..7de7e18389 100644 --- a/lib/NGCP/Panel/Controller/API/CustomerBalancesItem.pm +++ b/lib/NGCP/Panel/Controller/API/CustomerBalancesItem.pm @@ -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); } diff --git a/lib/NGCP/Panel/Controller/API/Customers.pm b/lib/NGCP/Panel/Controller/API/Customers.pm index 42456fc333..de0f8d610b 100644 --- a/lib/NGCP/Panel/Controller/API/Customers.pm +++ b/lib/NGCP/Panel/Controller/API/Customers.pm @@ -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; } diff --git a/lib/NGCP/Panel/Controller/API/CustomersItem.pm b/lib/NGCP/Panel/Controller/API/CustomersItem.pm index 30b548f114..93b220ac1e 100644 --- a/lib/NGCP/Panel/Controller/API/CustomersItem.pm +++ b/lib/NGCP/Panel/Controller/API/CustomersItem.pm @@ -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); } diff --git a/lib/NGCP/Panel/Controller/API/Root.pm b/lib/NGCP/Panel/Controller/API/Root.pm index aa684f82cf..03a9b5d640 100644 --- a/lib/NGCP/Panel/Controller/API/Root.pm +++ b/lib/NGCP/Panel/Controller/API/Root.pm @@ -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')) { diff --git a/lib/NGCP/Panel/Controller/API/TopupVouchers.pm b/lib/NGCP/Panel/Controller/API/TopupVouchers.pm index 5a293dd267..5fcab9c048 100644 --- a/lib/NGCP/Panel/Controller/API/TopupVouchers.pm +++ b/lib/NGCP/Panel/Controller/API/TopupVouchers.pm @@ -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; } diff --git a/lib/NGCP/Panel/Controller/Contract.pm b/lib/NGCP/Panel/Controller/Contract.pm index 2e28af2d47..0c6a4b7744 100644 --- a/lib/NGCP/Panel/Controller/Contract.pm +++ b/lib/NGCP/Panel/Controller/Contract.pm @@ -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; diff --git a/lib/NGCP/Panel/Controller/Customer.pm b/lib/NGCP/Panel/Controller/Customer.pm index 54486fdd1f..58d646ce62 100644 --- a/lib/NGCP/Panel/Controller/Customer.pm +++ b/lib/NGCP/Panel/Controller/Customer.pm @@ -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); } diff --git a/lib/NGCP/Panel/Controller/Invoice.pm b/lib/NGCP/Panel/Controller/Invoice.pm index d531eaa326..98498c49c9 100644 --- a/lib/NGCP/Panel/Controller/Invoice.pm +++ b/lib/NGCP/Panel/Controller/Invoice.pm @@ -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}, diff --git a/lib/NGCP/Panel/Controller/Reseller.pm b/lib/NGCP/Panel/Controller/Reseller.pm index 588a20f464..2e3752dd67 100644 --- a/lib/NGCP/Panel/Controller/Reseller.pm +++ b/lib/NGCP/Panel/Controller/Reseller.pm @@ -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( diff --git a/lib/NGCP/Panel/Controller/Root.pm b/lib/NGCP/Panel/Controller/Root.pm index 78cf662c29..09768f14b6 100644 --- a/lib/NGCP/Panel/Controller/Root.pm +++ b/lib/NGCP/Panel/Controller/Root.pm @@ -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; diff --git a/lib/NGCP/Panel/Field/ContractBalance.pm b/lib/NGCP/Panel/Field/ContractBalance.pm new file mode 100644 index 0000000000..ace4f95bae --- /dev/null +++ b/lib/NGCP/Panel/Field/ContractBalance.pm @@ -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: diff --git a/lib/NGCP/Panel/Form/Balance/BalanceIntervalAPI.pm b/lib/NGCP/Panel/Form/Balance/BalanceIntervalAPI.pm new file mode 100644 index 0000000000..74c3007d79 --- /dev/null +++ b/lib/NGCP/Panel/Form/Balance/BalanceIntervalAPI.pm @@ -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: \ No newline at end of file diff --git a/lib/NGCP/Panel/Form/CustomerBalance.pm b/lib/NGCP/Panel/Form/Balance/CustomerBalance.pm similarity index 94% rename from lib/NGCP/Panel/Form/CustomerBalance.pm rename to lib/NGCP/Panel/Form/Balance/CustomerBalance.pm index 1f009206cf..2b8119b42f 100644 --- a/lib/NGCP/Panel/Form/CustomerBalance.pm +++ b/lib/NGCP/Panel/Form/Balance/CustomerBalance.pm @@ -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 diff --git a/lib/NGCP/Panel/Form/Invoice/Invoice.pm b/lib/NGCP/Panel/Form/Invoice/Invoice.pm index 9a9746f778..212bda58cb 100644 --- a/lib/NGCP/Panel/Form/Invoice/Invoice.pm +++ b/lib/NGCP/Panel/Form/Invoice/Invoice.pm @@ -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', diff --git a/lib/NGCP/Panel/Model/DB.pm b/lib/NGCP/Panel/Model/DB.pm index 5563ca4279..1d9208177c 100644 --- a/lib/NGCP/Panel/Model/DB.pm +++ b/lib/NGCP/Panel/Model/DB.pm @@ -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 diff --git a/lib/NGCP/Panel/Role/API.pm b/lib/NGCP/Panel/Role/API.pm index c66b16b4a8..af94282bc3 100644 --- a/lib/NGCP/Panel/Role/API.pm +++ b/lib/NGCP/Panel/Role/API.pm @@ -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); diff --git a/lib/NGCP/Panel/Role/API/BalanceIntervals.pm b/lib/NGCP/Panel/Role/API/BalanceIntervals.pm new file mode 100644 index 0000000000..6be8e2db0f --- /dev/null +++ b/lib/NGCP/Panel/Role/API/BalanceIntervals.pm @@ -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: diff --git a/lib/NGCP/Panel/Role/API/Contracts.pm b/lib/NGCP/Panel/Role/API/Contracts.pm index 12a2e18014..6884aa23ea 100644 --- a/lib/NGCP/Panel/Role/API/Contracts.pm +++ b/lib/NGCP/Panel/Role/API/Contracts.pm @@ -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) { diff --git a/lib/NGCP/Panel/Role/API/CustomerBalances.pm b/lib/NGCP/Panel/Role/API/CustomerBalances.pm index b18c9c4db1..81036f357b 100644 --- a/lib/NGCP/Panel/Role/API/CustomerBalances.pm +++ b/lib/NGCP/Panel/Role/API/CustomerBalances.pm @@ -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 { diff --git a/lib/NGCP/Panel/Role/API/Customers.pm b/lib/NGCP/Panel/Role/API/Customers.pm index 2f7a37548b..a5dfb229f3 100644 --- a/lib/NGCP/Panel/Role/API/Customers.pm +++ b/lib/NGCP/Panel/Role/API/Customers.pm @@ -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) { diff --git a/lib/NGCP/Panel/Utils/Contract.pm b/lib/NGCP/Panel/Utils/Contract.pm index 5f22058653..d72bb0fb13 100644 --- a/lib/NGCP/Panel/Utils/Contract.pm +++ b/lib/NGCP/Panel/Utils/Contract.pm @@ -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'), diff --git a/lib/NGCP/Panel/Utils/DateTime.pm b/lib/NGCP/Panel/Utils/DateTime.pm index 677c5df4b6..35fdf10155 100644 --- a/lib/NGCP/Panel/Utils/DateTime.pm +++ b/lib/NGCP/Panel/Utils/DateTime.pm @@ -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; diff --git a/lib/NGCP/Panel/Utils/Form.pm b/lib/NGCP/Panel/Utils/Form.pm index d09896d40d..64d8c93451 100644 --- a/lib/NGCP/Panel/Utils/Form.pm +++ b/lib/NGCP/Panel/Utils/Form.pm @@ -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}); diff --git a/lib/NGCP/Panel/Utils/Journal.pm b/lib/NGCP/Panel/Utils/Journal.pm index 925388b7c4..f2177d2254 100644 --- a/lib/NGCP/Panel/Utils/Journal.pm +++ b/lib/NGCP/Panel/Utils/Journal.pm @@ -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 diff --git a/lib/NGCP/Panel/Utils/Message.pm b/lib/NGCP/Panel/Utils/Message.pm index 028be6841d..6daa3648a6 100644 --- a/lib/NGCP/Panel/Utils/Message.pm +++ b/lib/NGCP/Panel/Utils/Message.pm @@ -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(); diff --git a/lib/NGCP/Panel/Utils/ProfilePackages.pm b/lib/NGCP/Panel/Utils/ProfilePackages.pm index c819a701a1..4e7817f2f6 100644 --- a/lib/NGCP/Panel/Utils/ProfilePackages.pm +++ b/lib/NGCP/Panel/Utils/ProfilePackages.pm @@ -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; } diff --git a/lib/NGCP/Panel/Widget/Plugin/AdminBillingOverview.pm b/lib/NGCP/Panel/Widget/Plugin/AdminBillingOverview.pm index cd97f18090..b25598eb16 100644 --- a/lib/NGCP/Panel/Widget/Plugin/AdminBillingOverview.pm +++ b/lib/NGCP/Panel/Widget/Plugin/AdminBillingOverview.pm @@ -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'}, diff --git a/ngcp_panel.conf b/ngcp_panel.conf index 5a4627f939..6f97be7f3e 100644 --- a/ngcp_panel.conf +++ b/ngcp_panel.conf @@ -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 + + allow_fake_client_time 1 + allow_delay_commit 1 + + schema_class NGCP::Schema diff --git a/script/ngcp_panel_debug.sh b/script/ngcp_panel_debug.sh index 22931e976b..9e778e60f3 100755 --- a/script/ngcp_panel_debug.sh +++ b/script/ngcp_panel_debug.sh @@ -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 diff --git a/t/api-balanceintervals.t b/t/api-balanceintervals.t new file mode 100644 index 0000000000..30a8f5cb50 --- /dev/null +++ b/t/api-balanceintervals.t @@ -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; +} \ No newline at end of file diff --git a/t/api-customers.t b/t/api-customers.t index 996550a473..fd4c95d022 100644 --- a/t/api-customers.t +++ b/t/api-customers.t @@ -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'); diff --git a/t/api-pbxdevices.t b/t/api-pbxdevices.t index 0b7c3a4726..db1b8cb6d6 100644 --- a/t/api-pbxdevices.t +++ b/t/api-pbxdevices.t @@ -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')); diff --git a/t/api-threads.t b/t/api-threads.t new file mode 100644 index 0000000000..2a8952aba9 --- /dev/null +++ b/t/api-threads.t @@ -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; \ No newline at end of file diff --git a/t/lib/Test/Collection.pm b/t/lib/Test/Collection.pm index 24b873df99..84f6f1d88b 100644 --- a/t/lib/Test/Collection.pm +++ b/t/lib/Test/Collection.pm @@ -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',