MT#15477 topup log

any *attempt* of a top-up request should be
logged.

+create top-up log records for api
 in order to record failed top-up requests as well,
 the somewhat tricky thing is to have two
 separate transactions here
+propery casting of topup request json field
 values to numbers etc., so the transaction for
 creating the log entry cannot fail and e.g.
 requests with subscriber_id='blah' are recorded
 correctly
+new "request_token" parameter for /api/topupvoucher
 and /api/topupcash, to identify and filter for
 particular requests.
+topup log api resource
+topup log api tests
+fix to correctly reject used vouchers
+topup log panel UI
+fix for balanceintervals.t threaded tests

Change-Id: I86eb845f6173803705b12cc7e5cdbac9a3153a0a
changes/19/2819/6
Rene Krenn 10 years ago
parent 59dcde8cea
commit 7f7e3332d1

@ -28,7 +28,7 @@ class_has 'query_params' => (
default => sub {[
{
param => 'start',
description => 'Filter balance intervals starting after or with the specified time stamp.',
description => 'Filter balance intervals starting after or at the specified time stamp.',
query => {
first => sub {
my $q = shift;

@ -230,7 +230,7 @@ sub POST :Allow {
form => $form,
exceptions => [ "contact_id", "billing_profile_id", "profile_package_id" ],
);
$resource->{profile_package_id} = undef unless NGCP::Panel::Utils::ProfilePackages::ENABLE_PROFILE_PACKAGES;
#$resource->{profile_package_id} = undef unless NGCP::Panel::Utils::ProfilePackages::ENABLE_PROFILE_PACKAGES;
my $mappings_to_create = [];
last unless NGCP::Panel::Utils::Contract::prepare_billing_mappings(

@ -246,27 +246,36 @@ sub invalid_user : Private {
}
sub field_to_json : Private {
my ($self, $name) = @_;
my ($self, $field) = @_;
if ($field->$_isa('HTML::FormHandler::Field::Select')) {
return $self->field_to_select_options($field);
} # elsif { ... }
SWITCH: for ($name) {
SWITCH: for ($field->type) {
/Float|Integer|Money|PosInteger|Minute|Hour|MonthDay|Year/ &&
return "Number";
/Boolean/ &&
return "Boolean";
/Repeatable/ &&
return "Array";
/\+NGCP::Panel::Field::Select/ &&
return $self->field_to_select_options($field);
/\+NGCP::Panel::Field::Regex/ &&
return "String";
/\+NGCP::Panel::Field::DateTime/ &&
return "String";
/\+NGCP::Panel::Field::Country/ &&
return "String";
/\+NGCP::Panel::Field::EmailList/ &&
return "String";
/\+NGCP::Panel::Field::Identifier/ &&
return "String";
/\+NGCP::Panel::Field::SubscriberStatusSelect/ &&
/\+NGCP::Panel::Field::URI/ &&
return "String";
/\+NGCP::Panel::Field::SubscriberLockSelect/ &&
return "Number";
/\+NGCP::Panel::Field::IPAddress/ &&
return "String";
/\+NGCP::Panel::Field::E164/ &&
return "Object";
/Compound/ &&
@ -277,6 +286,8 @@ sub field_to_json : Private {
return "Array";
/\+NGCP::Panel::Field::PbxGroupMemberAPI/ &&
return "Array";
/\+NGCP::Panel::Field::Interval/ &&
return "Object";
# usually {xxx}{id}
/\+NGCP::Panel::Field::/ &&
return "Number";
@ -303,6 +314,7 @@ sub field_to_select_options : Private {
sub get_field_poperties :Private{
my ($self, $field) = @_;
my $name = $field->name;
return () if (
$field->type eq "Hidden" ||
$field->type eq "Button" ||
@ -312,7 +324,7 @@ sub get_field_poperties :Private{
push @types, 'null' unless ($field->required || $field->validate_when_empty);
my $type;
if($field->type =~ /^\+NGCP::Panel::Field::/) {
if($field->type =~ /E164/) {
if($field->type =~ /E164$/) {
$name = 'primary_number';
} elsif($field->type =~ /AliasNumber/) {
$name = 'alias_numbers';
@ -320,24 +332,21 @@ sub get_field_poperties :Private{
$name = 'pbx_group_ids';
} elsif($field->type =~ /Country$/) {
$name = 'country';
#} elsif($field->type !~ /Regex|EmailList|SubscriberStatusSelect|SubscriberLockSelect|Identifier|PosInteger/) {
# $name .= '_id';
#}
} elsif($field->type =~ /Select$/) {
$type = $self->field_to_select_options($field);
} elsif($field->type !~ /Regex|EmailList|Identifier|PosInteger|DateTime/) { #Interval, IPAddress, ...?
} elsif($field->type !~ /Regex|EmailList|Identifier|PosInteger|Interval|Select|DateTime|URI|IPAddress|DatePicker|ProfileNetwork/) { # ...?
$name .= '_id';
}
} elsif ($field->$_isa('HTML::FormHandler::Field::Select')) {
$type = $self->field_to_select_options($field);
}
push(@types, defined $type ? $type : $self->field_to_json($field->type));
my $desc;
}
push(@types, $self->field_to_json($field));
my $desc = undef;
if($field->element_attr) {
$desc = $field->element_attr->{title}->[0];
} else {
$desc = $name;
}
unless (defined $desc && length($desc) > 0) {
$desc = $field->label;
}
unless (defined $desc && length($desc) > 0) {
$desc = 'to be described ...';
}
return { name => $name, description => $desc, types => \@types };
}
sub get_collection_properties {

@ -76,16 +76,20 @@ sub OPTIONS :Allow {
sub POST :Allow {
my ($self, $c) = @_;
unless($c->user->billing_data) {
$c->log->error("user does not have billing data rights");
$self->error($c, HTTP_FORBIDDEN, "Unsufficient rights to create voucher");
return;
}
my $success = 0;
my $entities = {};
my $log_vals = {};
my $resource = undef;
my $now = NGCP::Panel::Utils::DateTime::current_local;
$c->model('DB')->set_transaction_isolation('READ COMMITTED');
my $guard = $c->model('DB')->txn_scope_guard;
{
my $resource = $self->get_valid_post_data(
unless($c->user->billing_data) {
$c->log->error("user does not have billing data rights");
$self->error($c, HTTP_FORBIDDEN, "Unsufficient rights to create voucher");
last;
}
$resource = $self->get_valid_post_data(
c => $c,
media_type => 'application/json',
);
@ -101,12 +105,11 @@ sub POST :Allow {
exceptions => [qw/package_id subscriber_id/],
);
my $now = NGCP::Panel::Utils::DateTime::current_local;
my $entities = {};
last unless NGCP::Panel::Utils::Voucher::check_topup(c => $c,
now => $now,
subscriber_id => $resource->{subscriber_id},
package_id => $resource->{package_id},
resource => $resource,
entities => $entities,
err_code => sub {
my ($err) = @_;
@ -114,26 +117,49 @@ sub POST :Allow {
$self->error($c, HTTP_UNPROCESSABLE_ENTITY, $err);
},
);
try {
my $balance = NGCP::Panel::Utils::ProfilePackages::topup_contract_balance(c => $c,
contract => $entities->{contract},
package => $entities->{package},
log_vals => $log_vals,
#old_package => $customer->profile_package,
amount => $resource->{amount},
now => $now,
request_token => $resource->{request_token},
subscriber => $entities->{subscriber},
);
} catch($e) {
$c->log->error("failed to perform cash topup: $e"); # TODO: user, message, trace, ...
$self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Failed to perform cash topup.");
last;
}
$guard->commit;
$success = 1;
$c->response->status(HTTP_NO_CONTENT);
$c->response->body(q());
}
undef $guard;
$guard = $c->model('DB')->txn_scope_guard;
{
try {
my $topup_log = NGCP::Panel::Utils::ProfilePackages::create_topup_log_record(
c => $c,
is_cash => 1,
now => $now,
entities => $entities,
log_vals => $log_vals,
resource => $resource,
is_success => $success
);
} catch($e) {
$c->log->error("failed to create topup log record: $e");
last;
}
$guard->commit;
}
return;
}

@ -0,0 +1,246 @@
package NGCP::Panel::Controller::API::TopupLogs;
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::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 =>
'Log of successful and failed <a href="#topupcash">TopupCash</a> and <a href="#topupvoucher">TopupVoucher</a> requests.',
);
class_has 'query_params' => (
is => 'ro',
isa => 'ArrayRef',
default => sub {[
{
param => 'reseller_id',
description => 'Filter for top-up requests for customers/subscribers of a specific reseller.',
query => {
first => sub {
my $q = shift;
return { 'contact.reseller.id' => $q };
},
second => sub {
return {
join => { 'contract' => 'contact' }
},
},
},
},
{
param => 'request_token',
description => 'Filter for top-up requests with the given request_token.',
query => {
first => sub {
my $q = shift;
return { 'me.request_token' => $q };
},
second => sub { },
},
},
{
param => 'timestamp_from',
description => 'Filter for top-up requests performed after or at the given time stamp.',
query => {
first => sub {
my $q = shift;
my $dt = NGCP::Panel::Utils::DateTime::from_string($q);
return { 'me.timestamp' => { '>=' => $dt->epoch } };
},
second => sub { },
},
},
{
param => 'timestamp_to',
description => 'Filter for top-up requests performed before or at the given time stamp.',
query => {
first => sub {
my $q = shift;
my $dt = NGCP::Panel::Utils::DateTime::from_string($q);
return { 'me.timestamp' => { '<=' => $dt->epoch } };
},
second => sub { },
},
},
{
param => 'contract_id',
description => 'Filter for top-up requests of a specific customer contract.',
query => {
first => sub {
my $q = shift;
return { 'me.contract_id' => $q };
},
second => sub {},
},
},
{
param => 'subscriber_id',
description => 'Filter for top-up requests of a specific subscriber.',
query => {
first => sub {
my $q = shift;
return { 'me.subscriber_id' => $q };
},
second => sub {},
},
},
{
param => 'voucher_id',
description => 'Filter for top-up requests with a specific voucher.',
query => {
first => sub {
my $q = shift;
return { 'me.voucher_id' => $q };
},
second => sub {},
},
},
{
param => 'outcome',
description => 'Filter for top-up requests by outcome.',
query => {
first => sub {
my $q = shift;
return { 'me.outcome' => $q };
},
second => sub { },
},
},
{
param => 'amount_above',
description => 'Filter for top-up requests with an amount greater than or equal to the given value in USD/EUR/etc.',
query => {
first => sub {
my $q = shift;
return { 'me.amount' => { '>=' => $q * 100.0 } };
},
second => sub { },
},
},
{
param => 'amount_below',
description => 'Filter for top-up requests with an amount less than or equal to the given value in USD/EUR/etc.',
query => {
first => sub {
my $q = shift;
return { 'me.amount' => { '<=' => $q * 100.0 } };
},
second => sub { },
},
},
]},
);
with 'NGCP::Panel::Role::API::TopupLogs';
class_has('resource_name', is => 'ro', default => 'topuplogs');
class_has('dispatch_path', is => 'ro', default => '/api/topuplogs/');
class_has('relation', is => 'ro', default => 'http://purl.org/sipwise/ngcp-api/#rel-topuplogs');
__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);
}
sub GET :Allow {
my ($self, $c) = @_;
my $page = $c->request->params->{page} // 1;
my $rows = $c->request->params->{rows} // 10;
{
my $items = $self->item_rs($c);
(my $total_count, $items) = $self->paginate_order_collection($c, $items);
my (@embedded, @links);
my $form = $self->get_form($c);
for my $item ($items->all) {
my $hal = $self->hal_from_item($c, $item, $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, $item->id),
);
$link->_forcearray(1);
push @links, $link;
}
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->log_response($c);
}

@ -0,0 +1,91 @@
package NGCP::Panel::Controller::API::TopupLogsItem;
use Sipwise::Base;
use namespace::sweep;
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::TopupLogs';
class_has('resource_name', is => 'ro', default => 'topuplogs');
class_has('dispatch_path', is => 'ro', default => '/api/topuplogs/');
class_has('relation', is => 'ro', default => 'http://purl.org/sipwise/ngcp-api/#rel-topuplogs');
__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 }
},
action_roles => [qw(HTTPMethods)],
);
sub auto :Private {
my ($self, $c) = @_;
$self->set_body($c);
$self->log_request($c);
}
sub GET :Allow {
my ($self, $c, $id) = @_;
{
last unless $self->valid_id($c, $id);
my $item = $self->item_by_id($c, $id);
last unless $self->resource_exists($c, topuplog => $item);
my $hal = $self->hal_from_item($c, $item);
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($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 end : Private {
my ($self, $c) = @_;
$self->log_response($c);
}
# vim: set tabstop=4 expandtab:

@ -76,16 +76,21 @@ sub OPTIONS :Allow {
sub POST :Allow {
my ($self, $c) = @_;
unless($c->user->billing_data) {
$c->log->error("user does not have billing data rights");
$self->error($c, HTTP_FORBIDDEN, "Unsufficient rights to create voucher");
return;
}
my $success = 0;
my $entities = {};
my $log_vals = {};
my $resource = undef;
my $now = NGCP::Panel::Utils::DateTime::current_local;
$c->model('DB')->set_transaction_isolation('READ COMMITTED');
my $guard = $c->model('DB')->txn_scope_guard;
{
my $resource = $self->get_valid_post_data(
unless($c->user->billing_data) {
$c->log->error("user does not have billing data rights");
$self->error($c, HTTP_FORBIDDEN, "Unsufficient rights to create voucher");
last;
}
$resource = $self->get_valid_post_data(
c => $c,
media_type => 'application/json',
);
@ -99,13 +104,11 @@ sub POST :Allow {
exceptions => [qw/subscriber_id/],
);
my $now = NGCP::Panel::Utils::DateTime::current_local;
my $entities = {};
last unless NGCP::Panel::Utils::Voucher::check_topup(c => $c,
now => $now,
subscriber_id => $resource->{subscriber_id},
plain_code => $resource->{code},
resource => $resource,
entities => $entities,
err_code => sub {
my ($err) = @_;
@ -118,7 +121,10 @@ sub POST :Allow {
my $balance = NGCP::Panel::Utils::ProfilePackages::topup_contract_balance(c => $c,
contract => $entities->{contract},
voucher => $entities->{voucher},
log_vals => $log_vals,
now => $now,
request_token => $resource->{request_token},
subscriber => $entities->{subscriber},
);
$entities->{voucher}->update({
@ -132,10 +138,30 @@ sub POST :Allow {
}
$guard->commit;
$success = 1;
$c->response->status(HTTP_NO_CONTENT);
$c->response->body(q());
}
undef $guard;
$guard = $c->model('DB')->txn_scope_guard;
{
try {
my $topup_log = NGCP::Panel::Utils::ProfilePackages::create_topup_log_record(
c => $c,
is_cash => 0,
now => $now,
entities => $entities,
log_vals => $log_vals,
resource => $resource,
is_success => $success
);
} catch($e) {
$c->log->error("failed to create topup log record: $e");
last;
}
$guard->commit;
}
return;
}

@ -179,7 +179,7 @@ sub create :Chained('list_customer') :PathPart('create') :Args(0) {
$form->values->{$_.'_id'} = $form->values->{$_}{id} || undef;
delete $form->values->{$_};
}
$form->values->{profile_package_id} = undef unless NGCP::Panel::Utils::ProfilePackages::ENABLE_PROFILE_PACKAGES;
#$form->values->{profile_package_id} = undef unless NGCP::Panel::Utils::ProfilePackages::ENABLE_PROFILE_PACKAGES;
$form->values->{create_timestamp} = $form->values->{modify_timestamp} = NGCP::Panel::Utils::DateTime::current_local;
$form->values->{external_id} = $form->field('external_id')->value;
unless($form->values->{max_subscribers} && length($form->values->{max_subscribers})) {
@ -313,7 +313,11 @@ sub base :Chained('list_customer') :PathPart('') :CaptureArgs(1) {
$c->stash->{balanceinterval_dt_columns} = NGCP::Panel::Utils::Datatables::set_columns($c, [
NGCP::Panel::Utils::ProfilePackages::get_balanceinterval_datatable_cols($c),
]);
]);
$c->stash->{topuplog_dt_columns} = NGCP::Panel::Utils::Datatables::set_columns($c, [
NGCP::Panel::Utils::ProfilePackages::get_topuplog_datatable_cols($c),
]);
my $product_id = $contract_rs->first->get_column('product_id');
NGCP::Panel::Utils::Message->error(
@ -467,7 +471,7 @@ sub edit :Chained('base_restricted') :PathPart('edit') :Args(0) {
$form->values->{$_.'_id'} = $form->values->{$_}{id} || undef;
delete $form->values->{$_};
}
$form->values->{profile_package_id} = undef unless NGCP::Panel::Utils::ProfilePackages::ENABLE_PROFILE_PACKAGES;
#$form->values->{profile_package_id} = undef unless NGCP::Panel::Utils::ProfilePackages::ENABLE_PROFILE_PACKAGES;
$form->values->{modify_timestamp} = $now; #problematic for ON UPDATE current_timestamp columns
$form->values->{external_id} = $form->field('external_id')->value;
unless($form->values->{max_subscribers} && length($form->values->{max_subscribers})) {
@ -987,16 +991,19 @@ sub topup_cash :Chained('base_restricted') :PathPart('balance/topupcash') :Args(
back_uri => $c->req->uri,
);
if($posted && $form->validated) {
my $success = 0;
my $entities = {};
my $log_vals = {};
try {
my $schema = $c->model('DB');
$schema->set_transaction_isolation('READ COMMITTED');
$schema->txn_do(sub {
my $entities = {};
NGCP::Panel::Utils::Voucher::check_topup(c => $c,
now => $now,
contract => $contract,
package_id => $form->values->{package}{id},
resource => $form->values,
entities => $entities,
err_code => sub {
my ($err) = @_;
@ -1007,13 +1014,16 @@ sub topup_cash :Chained('base_restricted') :PathPart('balance/topupcash') :Args(
my $balance = NGCP::Panel::Utils::ProfilePackages::topup_contract_balance(c => $c,
contract => $contract,
package => $entities->{package},
log_vals => $log_vals,
#old_package => $customer->profile_package,
amount => $form->values->{amount},
now => $now,
request_token => NGCP::Panel::Utils::ProfilePackages::PANEL_TOPUP_REQUEST_TOKEN,
);
delete $c->session->{created_objects}->{package};
});
$success = 1;
NGCP::Panel::Utils::Message->info(
c => $c,
desc => $c->loc('Top-up using cash performed successfully!'),
@ -1026,6 +1036,25 @@ sub topup_cash :Chained('base_restricted') :PathPart('balance/topupcash') :Args(
desc => $c->loc('Failed to top-up using cash!'),
);
}
try {
$c->model('DB')->txn_do(sub {
my $topup_log = NGCP::Panel::Utils::ProfilePackages::create_topup_log_record(
c => $c,
is_cash => 1,
now => $now,
entities => $entities,
log_vals => $log_vals,
resource => $form->values,
is_success => $success,
request_token => NGCP::Panel::Utils::ProfilePackages::PANEL_TOPUP_REQUEST_TOKEN,
);
});
}
catch($e) {
$c->log->error("failed to create topup log record: $e");
}
$c->response->redirect($c->uri_for_action("/customer/details", [$contract->id]));
return;
}
@ -1056,16 +1085,19 @@ sub topup_voucher :Chained('base_restricted') :PathPart('balance/topupvoucher')
back_uri => $c->req->uri,
);
if($posted && $form->validated) {
my $success = 0;
my $entities = {};
my $log_vals = {};
try {
my $schema = $c->model('DB');
$schema->set_transaction_isolation('READ COMMITTED');
$schema->txn_do(sub {
my $entities = {};
NGCP::Panel::Utils::Voucher::check_topup(c => $c,
now => $now,
contract => $contract,
voucher_id => $form->values->{voucher}{id},
resource => $form->values,
entities => $entities,
err_code => sub {
my ($err) = @_;
@ -1076,7 +1108,9 @@ sub topup_voucher :Chained('base_restricted') :PathPart('balance/topupvoucher')
my $balance = NGCP::Panel::Utils::ProfilePackages::topup_contract_balance(c => $c,
contract => $contract,
voucher => $entities->{voucher},
log_vals => $log_vals,
now => $now,
request_token => NGCP::Panel::Utils::ProfilePackages::PANEL_TOPUP_REQUEST_TOKEN,
);
$entities->{voucher}->update({
@ -1085,6 +1119,7 @@ sub topup_voucher :Chained('base_restricted') :PathPart('balance/topupvoucher')
});
});
$success = 1;
NGCP::Panel::Utils::Message->info(
c => $c,
desc => $c->loc('Top-up using voucher performed successfully!'),
@ -1097,6 +1132,25 @@ sub topup_voucher :Chained('base_restricted') :PathPart('balance/topupvoucher')
desc => $c->loc('Failed to top-up using voucher!'),
);
}
try {
$c->model('DB')->txn_do(sub {
my $topup_log = NGCP::Panel::Utils::ProfilePackages::create_topup_log_record(
c => $c,
is_cash => 0,
now => $now,
entities => $entities,
log_vals => $log_vals,
resource => $form->values,
is_success => $success,
request_token => NGCP::Panel::Utils::ProfilePackages::PANEL_TOPUP_REQUEST_TOKEN,
);
});
}
catch($e) {
$c->log->error("failed to create topup log record: $e");
}
$c->response->redirect($c->uri_for_action("/customer/details", [$contract->id]));
return;
}
@ -1113,6 +1167,13 @@ sub balanceinterval_ajax :Chained('base') :PathPart('balanceinterval/ajax') :Arg
$c->detach( $c->view("JSON") );
}
sub topuplog_ajax :Chained('base') :PathPart('topuplog/ajax') :Args(0) {
my ($self, $c) = @_;
my $res = $c->stash->{contract}->topup_log;
NGCP::Panel::Utils::Datatables::process($c, $res, $c->stash->{topuplog_dt_columns});
$c->detach( $c->view("JSON") );
}
sub subscriber_ajax :Chained('base') :PathPart('subscriber/ajax') :Args(0) {
my ($self, $c) = @_;
my $res = $c->stash->{contract}->voip_subscribers->search({

@ -43,5 +43,15 @@ has_field 'amount' => (
default => '0',
);
has_field 'request_token' => (
type => 'Text',
maxlength => 255,
element_attr => {
rel => ['tooltip'],
title => ['An external ID to identify the top-up request in the top-up log.']
},
required => 0,
);
1;
# vim: set tabstop=4 expandtab:

@ -0,0 +1,194 @@
package NGCP::Panel::Form::Topup::Log;
use HTML::FormHandler::Moose;
extends 'HTML::FormHandler';
use Moose::Util::TypeConstraints;
has_field 'id' => (
type => 'Hidden'
);
has_field 'username' => (
type => 'Text',
label => 'The user that attempted the topup.',
required => 1,
);
has_field 'timestamp' => (
type => '+NGCP::Panel::Field::DateTime',
label => 'The timestamp of the topup attempt.',
required => 1,
);
has_field 'type' => (
type => 'Select',
label => 'The top-up request type.',
options => [
{ value => 'cash', label => 'Cash top-up' },
{ value => 'voucher', label => 'Voucher top-up' },
],
required => 1,
);
has_field 'outcome' => (
type => 'Select',
label => 'The top-up operation outcome.',
options => [
{ value => 'ok', label => 'OK' },
{ value => 'failed', label => 'FAILED' },
],
required => 1,
);
has_field 'message' => (
type => 'Text',
label => 'The top-up request response message (error reason).',
required => 0,
);
has_field 'subscriber_id' => (
type => 'PosInteger',
label => 'The subscriber for which to topup the balance.',
required => 0,
);
has_field 'contract_id' => (
type => 'PosInteger',
label => 'The subscriber\'s customer contract.',
required => 0,
);
has_field 'amount' => (
type => 'Money',
required => 0,
inflate_method => \&inflate_money,
deflate_method => \&deflate_money,
label => 'The top-up amount in Euro/USD/etc.',
);
has_field 'voucher_id' => (
type => 'PosInteger',
label => 'The voucher in case of a voucher top-up.',
required => 0,
);
has_field 'voucher_id' => (
type => 'PosInteger',
label => 'The voucher in case of a voucher top-up.',
required => 0,
);
has_field 'cash_balance_before' => (
type => 'Money',
required => 0,
inflate_method => \&inflate_money,
deflate_method => \&deflate_money,
label => 'The contract\'s cash balance before the top-up in Euro/USD/etc.',
);
has_field 'cash_balance_after' => (
type => 'Money',
required => 0,
inflate_method => \&inflate_money,
deflate_method => \&deflate_money,
label => 'The contract\'s cash balance after the top-up in Euro/USD/etc.',
);
has_field 'package_before_id' => (
type => 'PosInteger',
label => 'The contract\'s profile package before the top-up.',
required => 0,
);
has_field 'package_after_id' => (
type => 'PosInteger',
label => 'The contract\'s profile package after the top-up.',
required => 0,
);
has_field 'profile_before_id' => (
type => 'PosInteger',
label => 'The contract\'s actual billing profile before the top-up.',
required => 0,
);
has_field 'profile_after_id' => (
type => 'PosInteger',
label => 'The contract\'s actual billing profile after the top-up.',
required => 0,
);
has_field 'lock_level_before' => (
type => 'Select',
label => 'The contract\'s subscribers\' lock levels before the top-up.',
options => [
{ value => '', label => '' },
{ value => '0', label => 'no lock (unlock)' },
{ value => '1', label => 'foreign' },
{ value => '2', label => 'outgoing' },
{ value => '3', label => 'all calls' },
{ value => '4', label => 'global' },
],
deflate_value_method => \&_deflate_lock_level,
inflate_default_method => \&_deflate_lock_level,
required => 0,
);
has_field 'lock_level_after' => (
type => 'Select',
label => 'The contract\'s subscribers\' lock levels after the top-up.',
options => [
{ value => '', label => '' },
{ value => '0', label => 'no lock (unlock)' },
{ value => '1', label => 'foreign' },
{ value => '2', label => 'outgoing' },
{ value => '3', label => 'all calls' },
{ value => '4', label => 'global' },
],
deflate_value_method => \&_deflate_lock_level,
inflate_default_method => \&_deflate_lock_level,
required => 0,
);
has_field 'contract_balance_before_id' => (
type => 'PosInteger',
label => 'The contract\'s balance interval before the top-up.',
required => 0,
);
has_field 'contract_balance_after_id' => (
type => 'PosInteger',
label => 'The contract\'s balance interval after the top-up.',
required => 0,
);
has_field 'request_token' => (
type => 'Text',
label => 'The external ID to identify top-up request.',
required => 0,
);
sub inflate_money {
return $_[1] * 100.0 if defined $_[1];
}
sub deflate_money {
return $_[1] / 100.0 if defined $_[1];
}
sub _deflate_lock_level {
my ($self,$value) = @_;
if (defined $value and length($value) == 0) {
return undef;
}
return $value;
}
sub _inflate_lock_level {
my ($self,$value) = @_;
if (!defined $value) {
return '';
}
return $value;
}
1;

@ -34,5 +34,15 @@ has_field 'code' => (
},
);
has_field 'request_token' => (
type => 'Text',
maxlength => 255,
element_attr => {
rel => ['tooltip'],
title => ['An external ID to identify the top-up request in the top-up log.']
},
required => 0,
);
1;
# vim: set tabstop=4 expandtab:

@ -193,6 +193,7 @@ sub error {
$c->response->content_type('application/json');
$c->response->status($code);
$c->response->body(JSON::to_json({ code => $code, message => $message })."\n");
$c->stash(api_error_message => $message);
return;
}

@ -61,6 +61,7 @@ sub hal_from_item {
Data::HAL::Link->new(relation => 'profile', href => 'http://purl.org/sipwise/ngcp-api/'),
Data::HAL::Link->new(relation => 'self', href => sprintf("%s%d", $self->dispatch_path, $item->contract->id)),
Data::HAL::Link->new(relation => 'ngcp:customers', href => sprintf("/api/customers/%d", $item->contract->id)),
Data::HAL::Link->new(relation => 'ngcp:balanceintervals', href => sprintf("/api/balanceintervals/%d/%d", $item->contract->id, $item->id)),
$self->get_journal_relation_link($item->contract->id),
],
relation => 'ngcp:'.$self->resource_name,

@ -164,7 +164,7 @@ sub update_customer {
resource => $resource,
exceptions => [ "contact_id", "billing_profile_id", "profile_package_id" ],
);
$resource->{profile_package_id} = undef unless NGCP::Panel::Utils::ProfilePackages::ENABLE_PROFILE_PACKAGES;
#$resource->{profile_package_id} = undef unless NGCP::Panel::Utils::ProfilePackages::ENABLE_PROFILE_PACKAGES;
#my $now = NGCP::Panel::Utils::DateTime::current_local;

@ -0,0 +1,97 @@
package NGCP::Panel::Role::API::TopupLogs;
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::Topup::Log;
use Data::Dumper;
sub item_rs {
my ($self, $c) = @_;
my $item_rs = $c->model('DB')->resultset('topup_logs');
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 get_form {
my ($self, $c) = @_;
return NGCP::Panel::Form::Topup::Log->new;
}
sub hal_from_item {
my ($self, $c, $item, $form) = @_;
my %resource = $item->get_inflated_columns;
my $datetime_fmt = DateTime::Format::Strptime->new(
pattern => '%F %T',
);
$resource{timestamp} = $datetime_fmt->format_datetime($resource{timestamp}) if defined $resource{timestamp};
$resource{amount} = $resource{amount} / 100.0 if defined $resource{amount};
$resource{cash_balance_before} = $resource{cash_balance_before} / 100.0 if defined $resource{cash_balance_before};
$resource{cash_balance_after} = $resource{cash_balance_after} / 100.0 if defined $resource{cash_balance_after};
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,
),
Data::HAL::Link->new(relation => 'collection', href => sprintf("/api/%s/", $self->resource_name)),
Data::HAL::Link->new(relation => 'profile', href => 'http://purl.org/sipwise/ngcp-api/'),
Data::HAL::Link->new(relation => 'self', href => sprintf("%s%d", $self->dispatch_path, $item->id)),
(defined $item->subscriber_id ? Data::HAL::Link->new(relation => 'ngcp:subscribers', href => sprintf("/api/subscribers/%d", $item->subscriber_id)) : ()),
(defined $item->contract_id ? Data::HAL::Link->new(relation => 'ngcp:customers', href => sprintf("/api/customers/%d", $item->contract_id)) : ()),
(defined $item->voucher_id ? Data::HAL::Link->new(relation => 'ngcp:vouchers', href => sprintf("/api/vouchers/%d", $item->voucher_id)) : ()),
(defined $item->profile_before_id ? Data::HAL::Link->new(relation => 'ngcp:billingprofiles', href => sprintf("/api/billingprofiles/%d", $item->profile_before_id)) : ()),
(defined $item->profile_after_id ? Data::HAL::Link->new(relation => 'ngcp:billingprofiles', href => sprintf("/api/billingprofiles/%d", $item->profile_after_id)) : ()),
(defined $item->package_before_id ? Data::HAL::Link->new(relation => 'ngcp:profilepackages', href => sprintf("/api/profilepackages/%d", $item->package_before_id)) : ()),
(defined $item->package_after_id ? Data::HAL::Link->new(relation => 'ngcp:profilepackages', href => sprintf("/api/profilepackages/%d", $item->package_after_id)) : ()),
(defined $item->contract_balance_before_id ? Data::HAL::Link->new(relation => 'ngcp:balanceintervals', href => sprintf("/api/balanceintervals/%d/%d", $item->contract_id, $item->contract_balance_before_id)) : ()),
(defined $item->contract_balance_after_id ? Data::HAL::Link->new(relation => 'ngcp:balanceintervals', href => sprintf("/api/balanceintervals/%d/%d", $item->contract_id, $item->contract_balance_after_id)) : ()),
],
relation => 'ngcp:'.$self->resource_name,
);
$form //= $self->get_form($c);
$self->validate_form(
c => $c,
resource => \%resource,
form => $form,
run => 0,
exceptions => [qw/id subscriber_id contract_id voucher_id package_before_id package_after_id profile_before_id profile_after_id contract_balance_before_id contract_balance_after_id/],
);
$resource{id} = int($item->id);
$hal->resource({%resource});
return $hal;
}
sub item_by_id {
my ($self, $c, $id) = @_;
my $item_rs = $self->item_rs($c);
return $item_rs->find($id);
}
1;

@ -169,6 +169,7 @@ method error ($self: Catalyst :$c, Str :$desc, :$log?, :$error?, :$type = 'panel
$usr_text,
$log_params->{tx_id},
}]);
$c->stash(panel_error_message => $msg);
}
return $rc;
}

@ -903,7 +903,29 @@ sub set_provisoning_voip_subscriber_first_int_attr_value {
$rs->create({ value => $new_value });
} # nothing to do for level 0, if no lock is set yet
} catch($e) {
$c->log->error("failed to set provisioning_voip_subscriber $attribute: $e");
$c->log->error("failed to set provisioning_voip_subscriber attribute '$attribute': $e");
$e->rethrow;
}
}
sub get_provisoning_voip_subscriber_first_int_attr_value {
my %params = @_;
my $c = $params{c};
my $prov_subscriber= $params{prov_subscriber};
my $attribute = $params{attribute};
return undef unless $prov_subscriber;
my $rs = NGCP::Panel::Utils::Preferences::get_usr_preference_rs(
c => $c,
prov_subscriber => $prov_subscriber,
attribute => $attribute,
);
try {
return $rs->first;
} catch($e) {
$c->log->error("failed to get provisioning_voip_subscriber attribute '$attribute': $e");
$e->rethrow;
}
}

@ -1,10 +1,12 @@
package NGCP::Panel::Utils::ProfilePackages;
use strict;
use warnings;
use Scalar::Util qw(looks_like_number);
#use TryCatch;
use NGCP::Panel::Utils::DateTime qw();
use NGCP::Panel::Utils::Subscriber qw();
use Data::Dumper;
use constant INITIAL_PROFILE_DISCRIMINATOR => 'initial';
use constant UNDERRUN_PROFILE_DISCRIMINATOR => 'underrun';
@ -37,10 +39,12 @@ use constant _DEFAULT_PROFILE_FREE_TIME => 0;
use constant _DEFAULT_PROFILE_FREE_CASH => 0.0;
#use constant _DEFAULT_NOTOPUP_DISCARD_INTERVALS => undef;
use constant ENABLE_PROFILE_PACKAGES => 1;
#use constant ENABLE_PROFILE_PACKAGES => 1;
use constant _ENABLE_UNDERRUN_PROFILES => 1;
use constant _ENABLE_UNDERRUN_LOCK => 1;
use constant PANEL_TOPUP_REQUEST_TOKEN => 'panel';
sub get_contract_balance {
my %params = @_;
my($c,$contract,$now,$schema,$stime,$etime) = @params{qw/c contract now schema stime etime/};
@ -134,6 +138,7 @@ sub resize_actual_contract_balance {
@$resized_balance_values,
});
$actual_balance->discard_changes();
$c->log->debug('contract ' . $contract->id . ' contract_balance row resized: ' . Dumper({ $actual_balance->get_inflated_columns }));
if ($create_next_balance) {
$actual_balance = catchup_contract_balances(c => $c,
contract => $contract,
@ -150,13 +155,15 @@ sub resize_actual_contract_balance {
#underruns due to increased thresholds:
my $update = {};
if (_ENABLE_UNDERRUN_LOCK && defined $underrun_lock_threshold && ($actual_balance->cash_balance + $topup_amount) < $underrun_lock_threshold) {
$c->log->debug('contract ' . $contract->id . ' cash balance is ' . ($actual_balance->cash_balance + $topup_amount) . ' and drops below underrun lock threshold ' . $underrun_lock_threshold) if $c;
if (defined $new_package->underrun_lock_level) {
set_subscriber_lock_level(c => $c, contract => $contract, lock_level => $new_package->underrun_lock_level);
$update->{underrun_lock} = $now;
}
}
if (_ENABLE_UNDERRUN_PROFILES && defined $underrun_profile_threshold && ($actual_balance->cash_balance + $topup_amount) < $underrun_profile_threshold) {
#my $bm_actual = get_actual_billing_mapping(schema => $schema, contract => $contract, now => $now);
#my $bm_actual = get_actual_billing_mapping(schema => $schema, contract => $contract, now => $now);
$c->log->debug('contract ' . $contract->id . ' cash balance is ' . ($actual_balance->cash_balance + $topup_amount) . ' and drops below underrun profile threshold ' . $underrun_profile_threshold) if $c;
if (add_profile_mappings(c => $c,
contract => $contract,
package => $new_package,
@ -173,7 +180,8 @@ sub resize_actual_contract_balance {
}
} else {
die("Another action finished meanwhile, please try again.");
$c->log->debug('attempt to resize contract ' . $contract->id . ' contract_balance row starting in the future') if $c;
die("Future balance interval detected. Please retry, if another top-up action finished meanwhile.");
}
return $actual_balance;
@ -193,7 +201,7 @@ sub catchup_contract_balances {
$topup_amount //= 0.0;
$profiles_added //= 0;
#$c->log->debug('catchup contract ' . $contract->id . ' contract_balances (now = ' . NGCP::Panel::Utils::DateTime::to_string($now) . ')') if $c;
$c->log->debug('catchup contract ' . $contract->id . ' contract_balances (now = ' . NGCP::Panel::Utils::DateTime::to_string($now) . ')');
my ($start_mode,$interval_unit,$interval_value,$carry_over_mode,$has_package,$notopup_discard_intervals,$underrun_profile_threshold,$underrun_lock_threshold);
@ -249,7 +257,7 @@ PREPARE_BALANCE_CATCHUP:
interval_value => $interval_value,
create => $contract_create);
my $balance_values = _get_balance_values(schema => $schema,
my $balance_values = _get_balance_values(schema => $schema,c => $c,
stime => $stime,
etime => $etime,
#start_mode => $start_mode,
@ -263,6 +271,7 @@ PREPARE_BALANCE_CATCHUP:
if (_ENABLE_UNDERRUN_LOCK && !$suppress_underrun && !$underrun_lock_applied && defined $underrun_lock_threshold && $last_balance->cash_balance >= $underrun_lock_threshold && ({ @$balance_values }->{cash_balance} + $topup_amount) < $underrun_lock_threshold) {
$underrun_lock_applied = 1;
$c->log->debug('contract ' . $contract->id . ' cash balance was decreased from ' . $last_balance->cash_balance . ' to ' . ({ @$balance_values }->{cash_balance} + $topup_amount) . ' and dropped below underrun lock threshold ' . $underrun_lock_threshold);
if (defined $old_package->underrun_lock_level) {
set_subscriber_lock_level(c => $c, contract => $contract, lock_level => $old_package->underrun_lock_level);
$underrun_lock_ts = $now;
@ -270,6 +279,7 @@ PREPARE_BALANCE_CATCHUP:
}
if (_ENABLE_UNDERRUN_PROFILES && !$suppress_underrun && !$underrun_profiles_applied && defined $underrun_profile_threshold && ($profiles_added > 0 || $last_balance->cash_balance >= $underrun_profile_threshold) && ({ @$balance_values }->{cash_balance} + $topup_amount) < $underrun_profile_threshold) {
$underrun_profiles_applied = 1;
$c->log->debug('contract ' . $contract->id . ' cash balance was decreased from ' . $last_balance->cash_balance . ' to ' . ({ @$balance_values }->{cash_balance} + $topup_amount) . ' and dropped below underrun profile threshold ' . $underrun_profile_threshold);
if (add_profile_mappings(c=> $c,
contract => $contract,
package => $old_package,
@ -293,6 +303,8 @@ PREPARE_BALANCE_CATCHUP:
@$balance_values,
});
$last_balance->discard_changes();
$c->log->debug('contract ' . $contract->id . ' contract_balance row created: ' . Dumper({ $last_balance->get_inflated_columns }));
}
# in case of "topup" or "topup_interval" start modes, the current interval end can be
@ -313,11 +325,15 @@ PREPARE_BALANCE_CATCHUP:
my $timely_end = (_CARRY_OVER_TIMELY_MODE eq $carry_over_mode ? _add_interval($last_balance->start,$interval_unit,$interval_value,undef)->subtract(seconds => 1) : undef);
if ((defined $notopup_expiration && $now >= $notopup_expiration)
|| (defined $timely_end && $now > $timely_end)) {
$c->log->debug('discarding contract ' . $contract->id . " cash balance (mode '$carry_over_mode'" .
(defined $timely_end ? ', timely end ' . NGCP::Panel::Utils::DateTime::to_string($timely_end) : '') .
(defined $notopup_expiration ? ', notopup expiration ' . NGCP::Panel::Utils::DateTime::to_string($notopup_expiration) : '') . ')');
my $update = {
cash_balance => 0
};
if (_ENABLE_UNDERRUN_LOCK && !$suppress_underrun && !$underrun_lock_applied && defined $underrun_lock_threshold && $last_balance->cash_balance >= $underrun_lock_threshold && ($update->{cash_balance} + $topup_amount) < $underrun_lock_threshold) {
$underrun_lock_applied = 1;
$c->log->debug('contract ' . $contract->id . ' cash balance was decreased from ' . $last_balance->cash_balance . ' to ' . ($update->{cash_balance} + $topup_amount) . ' and dropped below underrun lock threshold ' . $underrun_lock_threshold);
if (defined $old_package->underrun_lock_level) {
set_subscriber_lock_level(c => $c, contract => $contract, lock_level => $old_package->underrun_lock_level);
$update->{underrun_lock} = $now;
@ -325,6 +341,7 @@ PREPARE_BALANCE_CATCHUP:
}
if (_ENABLE_UNDERRUN_PROFILES && !$suppress_underrun && !$underrun_profiles_applied && defined $underrun_profile_threshold && ($profiles_added > 0 || $last_balance->cash_balance >= $underrun_profile_threshold) && ($update->{cash_balance} + $topup_amount) < $underrun_profile_threshold) {
$underrun_profiles_applied = 1;
$c->log->debug('contract ' . $contract->id . ' cash balance was decreased from ' . $last_balance->cash_balance . ' to ' . ($update->{cash_balance} + $topup_amount) . ' and dropped below underrun profile threshold ' . $underrun_profile_threshold);
if (add_profile_mappings(c=> $c,
contract => $contract,
package => $old_package,
@ -346,19 +363,42 @@ PREPARE_BALANCE_CATCHUP:
sub topup_contract_balance {
my %params = @_;
my($c,$contract,$package,$voucher,$amount,$now,$schema) = @params{qw/c contract package voucher amount now schema/};
my($c,$contract,$package,$voucher,$amount,$now,$request_token,$schema,$log_vals,$subscriber) = @params{qw/c contract package voucher amount now request_token schema log_vals subscriber/};
$schema //= $c->model('DB');
$schema = $c->model('DB');
$contract = lock_contracts(schema => $schema, contract_id => $contract->id);
$now //= NGCP::Panel::Utils::DateTime::current_local;
my $voucher_package = ($voucher ? $voucher->profile_package : $package);
my $old_package = $contract->profile_package;
$log_vals->{old_package} = ( $old_package ? { $old_package->get_inflated_columns } : undef) if $log_vals;
$package = $voucher_package // $old_package;
$log_vals->{new_package} = ( $package ? { $package->get_inflated_columns } : undef) if $log_vals;
my $topup_amount = ($voucher ? $voucher->amount : $amount) // 0.0;
$voucher_package = undef unless ENABLE_PROFILE_PACKAGES;
$package = undef unless ENABLE_PROFILE_PACKAGES;
#$voucher_package = undef unless ENABLE_PROFILE_PACKAGES;
#$package = undef unless ENABLE_PROFILE_PACKAGES;
$c->log->debug('topup' . ($request_token ? ' (request token ' . $request_token . ') ' : ' ') . 'contract ' . $contract->id . ' using ' . ($voucher ? 'voucher ' . $voucher->id : 'cash'));
my $balance = catchup_contract_balances(c => $c,
contract => $contract,
old_package => $old_package,
now => $now);
if ($log_vals) {
$log_vals->{old_balance} = { $balance->get_inflated_columns };
my $bm_actual = get_actual_billing_mapping(schema => $schema, contract => $contract, now => $now);
my $profile = $bm_actual->billing_mappings->first->billing_profile;
$log_vals->{old_profile} = { $profile->get_inflated_columns };
if ($subscriber) {
$log_vals->{old_lock_level} = NGCP::Panel::Utils::Subscriber::get_provisoning_voip_subscriber_lock_level(
c => $c,
prov_subscriber => $subscriber->provisioning_voip_subscriber,
) if ($subscriber->provisioning_voip_subscriber);
$log_vals->{new_lock_level} = $log_vals->{old_lock_level};
}
}
my $profiles_added = 0;
if ($package) { #always apply (old or new) topup profiles
@ -373,6 +413,7 @@ sub topup_contract_balance {
profiles => 'topup_profiles',
now => $now);
}
$log_vals->{amount} = $topup_amount if $log_vals;
if ($voucher_package && (!$old_package || $voucher_package->id != $old_package->id)) {
$contract->update({ profile_package_id => $voucher_package->id,
@ -381,11 +422,6 @@ sub topup_contract_balance {
$contract->discard_changes();
}
my $balance = catchup_contract_balances(c => $c,
contract => $contract,
old_package => $old_package,
now => $now);
my ($is_timely,$timely_duration_unit,$timely_duration_value) = (0,undef,undef);
if ($old_package
&& ($timely_duration_unit = $old_package->timely_duration_unit)
@ -419,14 +455,71 @@ sub topup_contract_balance {
$balance->update({ cash_balance => $balance->cash_balance + $topup_amount }); #add in new interval
$contract->discard_changes();
if ($log_vals) {
$log_vals->{new_balance} = { $balance->get_inflated_columns };
my $bm_actual = get_actual_billing_mapping(schema => $schema, contract => $contract, now => $now);
my $profile = $bm_actual->billing_mappings->first->billing_profile;
$log_vals->{new_profile} = { $profile->get_inflated_columns };
}
if ($package) {
set_subscriber_lock_level(c => $c, contract => $contract, lock_level => $package->topup_lock_level) if defined $package->topup_lock_level;
if ($package && defined $package->topup_lock_level) {
set_subscriber_lock_level(c => $c, contract => $contract, lock_level => $package->topup_lock_level);
$log_vals->{new_lock_level} = $package->topup_lock_level;
}
return $balance;
}
sub create_topup_log_record {
my %params = @_;
my($c,$is_cash,$now,$entities,$log_vals,$resource,$message,$is_success,$request_token) = @params{qw/c is_cash now entities log_vals resource message is_success request_token/};
$resource //= {};
$resource->{contract_id} = $resource->{contract}{id} if (exists $resource->{contract} && 'HASH' eq ref $resource->{contract});
$resource->{subscriber_id} = $resource->{subscriber}{id} if (exists $resource->{subscriber} && 'HASH' eq ref $resource->{subscriber});
$resource->{voucher_id} = $resource->{voucher}{id} if (exists $resource->{voucher} && 'HASH' eq ref $resource->{voucher});
$resource->{package_id} = $resource->{package}{id} if (exists $resource->{package} && 'HASH' eq ref $resource->{package});
$resource->{contract_id} = undef if (exists $resource->{contract_id} && !looks_like_number($resource->{contract_id}));
$resource->{subscriber_id} = undef if (exists $resource->{subscriber_id} && !looks_like_number($resource->{subscriber_id}));
$resource->{voucher_id} = undef if (exists $resource->{voucher_id} && !looks_like_number($resource->{voucher_id}));
$resource->{package_id} = undef if (exists $resource->{package_id} && !looks_like_number($resource->{package_id}));
$resource->{amount} = undef if (exists $resource->{amount} && !looks_like_number($resource->{amount}));
my $username;
if($c->user->roles eq 'admin' || $c->user->roles eq 'reseller') {
$username = $c->user->login;
} elsif($c->user->roles eq 'subscriber' || $c->user->roles eq 'subscriberadmin') {
$username = $c->user->webusername . '@' . $c->user->domain->domain;
}
$message //= $c->stash->{api_error_message} // $c->stash->{panel_error_message};
return $c->model('DB')->resultset('topup_logs')->create({
username => $username,
timestamp => $now->hires_epoch,
type => ($is_cash ? 'cash' : 'voucher'),
outcome => ($is_success ? 'ok' : 'failed'),
message => (defined $message ? substr($message,0,255) : undef),
subscriber_id => ($entities->{subscriber} ? $entities->{subscriber}->id : $resource->{subscriber_id}),
contract_id => ($entities->{contract} ? $entities->{contract}->id : $resource->{contract_id}),
amount => (exists $log_vals->{amount} ? $log_vals->{amount} : (exists $resource->{amount} ? $resource->{amount} : undef)),
voucher_id => ($entities->{voucher} ? $entities->{voucher}->id : $resource->{voucher_id}),
cash_balance_before => (exists $log_vals->{old_balance} ? $log_vals->{old_balance}->{cash_balance} : undef),
cash_balance_after => (exists $log_vals->{new_balance} ? $log_vals->{new_balance}->{cash_balance} : undef),
package_before_id => (exists $log_vals->{old_package} && defined $log_vals->{old_package} ? $log_vals->{old_package}->{id} : undef),
package_after_id => (exists $log_vals->{new_package} && defined $log_vals->{new_package} ? $log_vals->{new_package}->{id} : ($entities->{package} ? $entities->{package}->id : $resource->{package_id})),
profile_before_id => (exists $log_vals->{old_profile} ? $log_vals->{old_profile}->{id} : undef),
profile_after_id => (exists $log_vals->{new_profile} ? $log_vals->{new_profile}->{id} : undef),
lock_level_before => (exists $log_vals->{old_lock_level} ? $log_vals->{old_lock_level} : undef),
lock_level_after => (exists $log_vals->{new_lock_level} ? $log_vals->{new_lock_level} : undef),
contract_balance_before_id => (exists $log_vals->{old_balance} ? $log_vals->{old_balance}->{id} : undef),
contract_balance_after_id => (exists $log_vals->{new_balance} ? $log_vals->{new_balance}->{id} : undef),
request_token => substr((defined $request_token ? $request_token : $resource->{request_token}),0,255),
});
}
sub create_initial_contract_balances {
my %params = @_;
my($c,$contract,$now) = @params{qw/c contract now/};
@ -476,6 +569,7 @@ PREPARE_BALANCE_INITIAL:
if (_ENABLE_UNDERRUN_LOCK && !$underrun_lock_applied && defined $package && defined $underrun_lock_threshold && { @$balance_values }->{cash_balance} < $underrun_lock_threshold) {
$underrun_lock_applied = 1;
$c->log->debug('contract ' . $contract->id . ' cash balance of ' . { @$balance_values }->{cash_balance} . ' is below underrun lock threshold ' . $underrun_lock_threshold);
if (defined $package->underrun_lock_level) {
set_subscriber_lock_level(c => $c, contract => $contract, lock_level => $package->underrun_lock_level);
$underrun_lock_ts = $now;
@ -483,6 +577,7 @@ PREPARE_BALANCE_INITIAL:
}
if (_ENABLE_UNDERRUN_PROFILES && !$underrun_profiles_applied && defined $package && defined $underrun_profile_threshold && { @$balance_values }->{cash_balance} < $underrun_profile_threshold) {
$underrun_profiles_applied = 1;
$c->log->debug('contract ' . $contract->id . ' cash balance of ' . { @$balance_values }->{cash_balance} . ' is below underrun profile threshold ' . $underrun_profile_threshold);
if (add_profile_mappings(c => $c,
contract => $contract,
package => $package,
@ -556,7 +651,7 @@ sub _get_balance_values {
if ($last_balance) {
if ((_CARRY_OVER_MODE eq $carry_over_mode
|| (_CARRY_OVER_TIMELY_MODE eq $carry_over_mode && $last_balance->timely_topup_count > 0)
) && (!$notopup_expiration || $stime < $notopup_expiration)) {
) && (!defined $notopup_expiration || $stime < $notopup_expiration)) {
#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;
@ -572,6 +667,8 @@ sub _get_balance_values {
$cash_balance += $last_balance->cash_balance_interval - $old_free_cash;
}
#$ratio * $last_profile->interval_free_time // _DEFAULT_PROFILE_FREE_TIME
} else {
$c->log->debug('discarding contract ' . $contract->id . " cash balance (mode '$carry_over_mode'" . (defined $notopup_expiration ? ', notopup expiration ' . NGCP::Panel::Utils::DateTime::to_string($notopup_expiration) : '') . ')') if $c;
}
$ratio = 1.0;
} else {
@ -776,6 +873,7 @@ sub underrun_lock_subscriber {
$underrun_lock_level = $package->underrun_lock_level;
}
if (defined $underrun_lock_threshold && defined $underrun_lock_level && $balance->cash_balance < $underrun_lock_threshold) {
$c->log->debug('contract ' . $contract->id . ' cash balance of ' . $balance->cash_balance . ' is below underrun lock threshold ' . $underrun_lock_threshold);
NGCP::Panel::Utils::Subscriber::lock_provisoning_voip_subscriber(
c => $c,
prov_subscriber => $subscriber->provisioning_voip_subscriber,
@ -787,7 +885,7 @@ sub underrun_lock_subscriber {
sub underrun_update_balance {
my %params = @_;
my ($c,$balance,$new_cash_balance,$now,$schema) = @params{qw/c balance new_cash_balance now schema/};
$schema //= $c->model('DB');
$schema = $c->model('DB');
$now //= NGCP::Panel::Utils::DateTime::current_local;
my $contract = $balance->contract;
my $package = $contract->profile_package;
@ -798,12 +896,14 @@ sub underrun_update_balance {
}
my $update = {};
if (_ENABLE_UNDERRUN_LOCK && defined $underrun_lock_threshold && $balance->cash_balance >= $underrun_lock_threshold && $new_cash_balance < $underrun_lock_threshold) {
$c->log->debug('contract ' . $contract->id . ' cash balance was set from ' . $balance->cash_balance . ' to ' . $new_cash_balance . ' and is now below underrun lock threshold ' . $underrun_lock_threshold);
if (defined $package->underrun_lock_level) {
set_subscriber_lock_level(c => $c, contract => $contract, lock_level => $package->underrun_lock_level);
$update->{underrun_lock} = $now;
}
}
if (_ENABLE_UNDERRUN_PROFILES && defined $underrun_profile_threshold && $balance->cash_balance >= $underrun_profile_threshold && $new_cash_balance < $underrun_profile_threshold) {
$c->log->debug('contract ' . $contract->id . ' cash balance was set from ' . $balance->cash_balance . ' to ' . $new_cash_balance . ' and is now below underrun profile threshold ' . $underrun_profile_threshold);
#my $bm_actual = get_actual_billing_mapping(schema => $schema, contract => $contract, now => $now);
if (add_profile_mappings(c => $c,
contract => $contract,
@ -1177,5 +1277,35 @@ sub get_balanceinterval_datatable_cols {
{ name => "underrun_lock", search => 0, title => $c->loc('Underrun (Lock)') },
);
}
sub get_topuplog_datatable_cols {
my ($c) = @_;
return ( #{ name => "id", search => 1, title => $c->loc("#") },
{ name => "timestamp", search_from_epoch => 1, search_to_epoch => 1, title => $c->loc('Timestamp') },
#{ name => "username", search => 1, title => $c->loc('User') },
{ name => "subscriber.username", search => 1, title => $c->loc('Subscriber') },
{ name => "type", search => 1, title => $c->loc('Type') },
{ name => "outcome", search => 1, title => $c->loc('Outcome') },
{ name => "message", search => 1, title => $c->loc('Message'),
literal_sql => "if(length(message) > 30, concat(left(message, 30), '...'), message)" },
{ name => "voucher_id", search => 1, title => $c->loc('Voucher ID') },
{ name => "amount", search => 0, title => $c->loc('Amount'), literal_sql => "FORMAT(amount / 100,2)" },
{ name => "cash_balance_before", search => 0, title => $c->loc('Balance before'), literal_sql => "FORMAT(cash_balance_before / 100,2)" },
{ name => "cash_balance_after", search => 0, title => $c->loc('Balance after'), literal_sql => "FORMAT(cash_balance_after / 100,2)" },
{ name => "old_package.name", search => 1, title => $c->loc('Package before') },
{ name => "new_package.name", search => 1, title => $c->loc('Package after') },
#{ name => "old_profile.name", search => 1, title => $c->loc('Profile before') },
#{ name => "new_profile.name", search => 1, title => $c->loc('Profile after') },
);
}
1;

@ -106,6 +106,14 @@ sub lock_provisoning_voip_subscriber {
);
}
sub get_provisoning_voip_subscriber_lock_level {
my %params = @_;
NGCP::Panel::Utils::Preferences::get_provisoning_voip_subscriber_first_int_attr_value(%params,
attribute => 'lock'
);
}
sub switch_prepaid {
my %params = @_;

@ -44,7 +44,7 @@ sub decrypt_code {
sub check_topup {
my %params = @_;
my ($c,$plain_code,$voucher_id,$now,$subscriber_id,$contract,$package_id,$schema,$err_code,$entities) = @params{qw/c plain_code voucher_id now subscriber_id contract package_id schema err_code entities/};
my ($c,$plain_code,$voucher_id,$now,$subscriber_id,$contract,$package_id,$schema,$err_code,$entities,$resource) = @params{qw/c plain_code voucher_id now subscriber_id contract package_id schema err_code entities resource/};
$schema //= $c->model('DB');
$now //= NGCP::Panel::Utils::DateTime::current_local;
@ -62,8 +62,13 @@ sub check_topup {
if (defined $subscriber_id) {
my $subscriber = $schema->resultset('voip_subscribers')->find($subscriber_id);
unless($subscriber) {
return 0 unless &{$err_code}('Unknown subscriber_id.');
if (defined $resource) {
$resource->{subscriber_id} = undef if exists $resource->{subscriber_id};
$resource->{subscriber}->{id} = undef if (exists $resource->{subscriber} && exists $resource->{subscriber}->{id});
}
return 0 unless &{$err_code}("Unknown subscriber ID $subscriber_id.");
}
$entities->{subscriber} = $subscriber if defined $entities;
$contract //= $subscriber->contract;
}
@ -86,30 +91,40 @@ sub check_topup {
my $voucher;
if (defined $plain_code) {
$voucher = $schema->resultset('vouchers')->find({
$voucher = $schema->resultset('vouchers')->search_rs({
code => encrypt_code($c, $plain_code),
used_at => undef, #used_by_subscriber_id => undef,
valid_until => { '<=' => $now },
used_at => { '=' => \"'0000-00-00 00:00:00'" } , #used_by_subscriber_id => undef,
valid_until => { '>=' => $now },
reseller_id => $contract->contact->reseller_id,
},{
for => 'update',
});
})->first;
unless($voucher) {
return 0 unless &{$err_code}('Invalid voucher code or already used.');
if (defined $resource) {
$resource->{voucher_id} = undef if exists $resource->{voucher_id};
$resource->{voucher}->{id} = undef if (exists $resource->{voucher} && exists $resource->{voucher}->{id});
}
return 0 unless &{$err_code}("Invalid voucher code '$plain_code', already used or expired.");
}
} else {
$voucher = $schema->resultset('vouchers')->find({
$voucher = $schema->resultset('vouchers')->search_rs({
id => $voucher_id,
used_at => undef, #used_by_subscriber_id => undef,
valid_until => { '<=' => $now },
used_at => { '=' => \"'0000-00-00 00:00:00'" }, #used_by_subscriber_id => undef,
valid_until => { '>=' => $now },
reseller_id => $contract->contact->reseller_id,
},{
for => 'update',
});
})->first;
unless($voucher) {
return 0 unless &{$err_code}('Invalid voucher ID or already used.');
if (defined $resource) {
$resource->{voucher_id} = undef if exists $resource->{voucher_id};
$resource->{voucher}->{id} = undef if (exists $resource->{voucher} && exists $resource->{voucher}->{id});
}
return 0 unless &{$err_code}("Invalid voucher ID $voucher_id, already used or expired.");
}
}
$entities->{voucher} = $voucher if defined $entities;
if($voucher->customer_id && $contract->id != $voucher->customer_id) {
return 0 unless &{$err_code}('Voucher is reserved for a different customer.');
@ -124,12 +139,16 @@ sub check_topup {
if (defined $package_id) {
$package = $schema->resultset('profile_packages')->find($package_id);
unless($package) {
return 0 unless &{$err_code}('Unknown profile package ID.');
if (defined $resource) {
$resource->{package_id} = undef if exists $resource->{package_id};
$resource->{package}->{id} = undef if (exists $resource->{package} && exists $resource->{package}->{id});
}
return 0 unless &{$err_code}("Unknown profile package ID $package_id.");
}
$entities->{package} = $package if defined $entities;
if(defined $reseller_id && $reseller_id != $package->reseller_id) {
return 0 unless &{$err_code}('Profile package belongs to another reseller.');
}
$entities->{package} = $package if defined $entities;
}
}

@ -450,6 +450,28 @@
</div>
</div>
<div class="accordion-group">
<div class="accordion-heading">
<a class="accordion-toggle" data-toggle="collapse" data-parent="#customer_details" href="#collapse_topuplog">[% c.loc('Top-up Log') %]</a>
</div>
<div class="accordion-body collapse" id="collapse_topuplog">
<div class="accordion-inner">
[%
helper.name = c.loc('Topuplog');
helper.identifier = 'topup_logs';
helper.dt_columns = topuplog_dt_columns;
helper.ajax_uri = c.uri_for_action('/customer/topuplog_ajax', [ c.req.captures.0 ]);
helper.top_buttons = [];
helper.dt_buttons = [];
PROCESS 'helpers/datatables.tt';
%]
</div>
</div>
</div>
[% IF c.user.roles == 'admin' || c.user.roles == 'reseller' %]
<div class="accordion-group">

@ -30,7 +30,7 @@ use NGCP::Panel::Utils::DateTime qw();
my $is_local_env = 0;
#my $enable_profile_packages = NGCP::Panel::Utils::ProfilePackages::ENABLE_PROFILE_PACKAGES;
my $enable_profile_packages = 1;
#my $enable_profile_packages = 1;
use Config::General;
my $catalyst_config;
@ -114,95 +114,7 @@ my $infinite_future;
my $t = time;
my $default_reseller_id = 1;
$req = HTTP::Request->new('POST', $uri.'/api/customercontacts/');
$req->header('Content-Type' => 'application/json');
$req->content(JSON::to_json({
firstname => "cust_contact_1_first",
lastname => "cust_contact_1_last",
email => "cust_contact1\@custcontact.invalid",
reseller_id => $default_reseller_id,
}));
$res = $ua->request($req);
is($res->code, 201, "create customer contact 1");
$req = HTTP::Request->new('GET', $uri.'/'.$res->header('Location'));
$res = $ua->request($req);
is($res->code, 200, "fetch customer contact 1");
my $custcontact1 = JSON::from_json($res->decoded_content);
$req = HTTP::Request->new('POST', $uri.'/api/customercontacts/');
$req->header('Content-Type' => 'application/json');
$req->content(JSON::to_json({
firstname => "cust_contact_2_first",
lastname => "cust_contact_2_last",
email => "cust_contact2\@custcontact.invalid",
reseller_id => $default_reseller_id,
}));
$res = $ua->request($req);
is($res->code, 201, "create customer contact 2");
$req = HTTP::Request->new('GET', $uri.'/'.$res->header('Location'));
$res = $ua->request($req);
is($res->code, 200, "fetch customer contact 2");
my $custcontact2 = JSON::from_json($res->decoded_content);
$req = HTTP::Request->new('POST', $uri.'/api/customercontacts/');
$req->header('Content-Type' => 'application/json');
$req->content(JSON::to_json({
firstname => "cust_contact_3_first",
lastname => "cust_contact_3_last",
email => "cust_contact3\@custcontact.invalid",
reseller_id => $default_reseller_id,
}));
$res = $ua->request($req);
is($res->code, 201, "create customer contact 3");
$req = HTTP::Request->new('GET', $uri.'/'.$res->header('Location'));
$res = $ua->request($req);
is($res->code, 200, "fetch customer contact 3");
my $custcontact3 = JSON::from_json($res->decoded_content);
$req = HTTP::Request->new('POST', $uri.'/api/customercontacts/');
$req->header('Content-Type' => 'application/json');
$req->content(JSON::to_json({
firstname => "cust_contact_4_first",
lastname => "cust_contact_4_last",
email => "cust_contact4\@custcontact.invalid",
reseller_id => $default_reseller_id,
}));
$res = $ua->request($req);
is($res->code, 201, "create customer contact 4");
$req = HTTP::Request->new('GET', $uri.'/'.$res->header('Location'));
$res = $ua->request($req);
is($res->code, 200, "fetch customer contact 4");
my $custcontact4 = JSON::from_json($res->decoded_content);
$req = HTTP::Request->new('POST', $uri.'/api/customercontacts/');
$req->header('Content-Type' => 'application/json');
$req->content(JSON::to_json({
firstname => "cust_contact_5_first",
lastname => "cust_contact_5_last",
email => "cust_contact5\@custcontact.invalid",
reseller_id => $default_reseller_id,
}));
$res = $ua->request($req);
is($res->code, 201, "create customer contact 5");
$req = HTTP::Request->new('GET', $uri.'/'.$res->header('Location'));
$res = $ua->request($req);
is($res->code, 200, "fetch customer contact 5");
my $custcontact5 = JSON::from_json($res->decoded_content);
$req = HTTP::Request->new('POST', $uri.'/api/customercontacts/');
$req->header('Content-Type' => 'application/json');
$req->content(JSON::to_json({
firstname => "cust_contact_9_first",
lastname => "cust_contact_9_last",
email => "cust_contact9\@custcontact.invalid",
reseller_id => $default_reseller_id,
}));
$res = $ua->request($req);
is($res->code, 201, "create customer contact 9");
$req = HTTP::Request->new('GET', $uri.'/'.$res->header('Location'));
$res = $ua->request($req);
is($res->code, 200, "fetch customer contact 9");
my $custcontact9 = JSON::from_json($res->decoded_content);
my $default_custcontact = _create_customer_contact();
$req = HTTP::Request->new('POST', $uri.'/api/domains/');
$req->header('Content-Type' => 'application/json');
@ -221,6 +133,7 @@ my $domain = JSON::from_json($res->decoded_content);
my %customer_map :shared = ();
my %subscriber_map :shared = ();
my $customer_contact_map = {};
my $package_map = {};
my $voucher_map = {};
my $profile_map = {};
@ -231,10 +144,49 @@ my $tb;
my $tb_cnt;
my $gantt_events;
if (_get_allow_fake_client_time() && $enable_profile_packages) {
if (_get_allow_fake_client_time()) { # && $enable_profile_packages) {
#goto SKIP;
#goto THREADED;
if ('Europe/Vienna' eq NGCP::Panel::Utils::DateTime::current_local()->time_zone->name) {
my $package = _create_profile_package('create','hour',1);
my $dt = NGCP::Panel::Utils::DateTime::from_string('2015-10-25 01:27:00');
ok($dt->is_dst(),NGCP::Panel::Utils::DateTime::to_string($dt)." is in daylight saving time (summer)");
_set_time($dt);
my $customer = _create_customer($package,'hourly_interval_dst_at');
_check_interval_history($customer,[
{ start => '2015-10-25 00:00:00', stop => '2015-10-25 00:59:59' },
{ start => '2015-10-25 01:00:00', stop => '2015-10-25 01:59:59' },
]);
$dt = NGCP::Panel::Utils::DateTime::from_string('2015-10-25 02:27:00');
ok(!$dt->is_dst(),NGCP::Panel::Utils::DateTime::to_string($dt)." is not in daylight saving time (winter)");
_set_time($dt);
_check_interval_history($customer,[
{ start => '2015-10-25 00:00:00', stop => '2015-10-25 00:59:59' },
{ start => '2015-10-25 01:00:00', stop => '2015-10-25 01:59:59' },
{ start => '2015-10-25 02:00:00', stop => '2015-10-25 02:59:59' },
]);
$dt = NGCP::Panel::Utils::DateTime::from_string('2015-10-25 03:27:00');
ok(!$dt->is_dst(),NGCP::Panel::Utils::DateTime::to_string($dt)." is not in daylight saving time (winter)");
_set_time($dt);
_check_interval_history($customer,[
{ start => '2015-10-25 00:00:00', stop => '2015-10-25 00:59:59' },
{ start => '2015-10-25 01:00:00', stop => '2015-10-25 01:59:59' },
{ start => '2015-10-25 02:00:00', stop => '2015-10-25 02:59:59' },
{ start => '2015-10-25 03:00:00', stop => '2015-10-25 03:59:59' },
]);
_set_time();
} else {
diag("time zone '" . NGCP::Panel::Utils::DateTime::current_local()->time_zone->name . "', skipping DST test");
}
{
my $package = _create_profile_package('create','hour',1);
@ -703,8 +655,8 @@ if (_get_allow_fake_client_time() && $enable_profile_packages) {
$req_identifier = $cnt . '. switch customer ' . $customer->{id} . ' to package ' . $prof_package_topup->{description}; diag($req_identifier); $cnt++;
$customer = _switch_package($customer,$prof_package_topup);
push(@$gantt_events,{ name => $req_identifier, t => $ts });
diag("wait a second here");
sleep(1); #sigh
#diag("wait a second here");
#sleep(1); #sigh
$req_identifier = $cnt . '. get balance history of customer ' . $customer->{id}; diag($req_identifier); $cnt++;
_check_interval_history($customer,[
{ start => '2014-10-01 00:00:00', stop => '~2014-10-04 13:00:00', package_id => undef },
@ -965,13 +917,15 @@ if (_get_allow_fake_client_time() && $enable_profile_packages) {
}
if (_get_allow_delay_commit()) {
my $custcontact1 = _create_customer_contact();
my $custcontact2 = _create_customer_contact();
_set_time(NGCP::Panel::Utils::DateTime::current_local->subtract(months => 3));
_create_customers_threaded(3,undef,undef,$custcontact9);
_create_customers_threaded(3,undef,undef,$custcontact1);
_create_customers_threaded(3,undef,undef,$custcontact2);
_set_time();
my $t1 = time;
my $delay = 10; #15;
my $delay = 5; #15;
my $t_a = threads->create(\&_fetch_customerbalances_worker,$delay,'id','asc',$custcontact2);
my $t_b = threads->create(\&_fetch_customerbalances_worker,$delay,'id','desc',$custcontact2);
@ -989,12 +943,13 @@ if (_get_allow_fake_client_time() && $enable_profile_packages) {
if (!is_deeply($got_desc,[ reverse @{ $got_asc } ],'compare customerbalances collection results of threaded requests deeply')) {
diag(Dumper({asc => $got_asc, desc => $got_desc}));
}
ok($t2 - $t1 > 2*$delay,'expected delay to assume customerbalances requests were processed after another');
my $delta_serialized = $t2 - $t1;
ok($delta_serialized > 2*$delay,'expected delay to assume customerbalances requests were processed after another');
#ok($t2 - $t1 < 3*$delay,'expected delay to assume only required contracts were locked');
$t1 = time;
$t_a = threads->create(\&_fetch_customerbalances_worker,$delay,'id','asc',$custcontact2);
$t_b = threads->create(\&_fetch_customerbalances_worker,$delay,'id','desc',$custcontact9);
$t_a = threads->create(\&_fetch_customerbalances_worker,$delay,'id','asc',$custcontact1);
$t_b = threads->create(\&_fetch_customerbalances_worker,$delay,'id','desc',$custcontact2);
#$t_c = threads->create(\&_fetch_customerbalances_worker,$delay,'id','asc',$custcontact2);
$intervals_a = $t_a->join();
$intervals_b = $t_b->join();
@ -1002,26 +957,29 @@ if (_get_allow_fake_client_time() && $enable_profile_packages) {
$t2 = time;
is($intervals_a->{total_count},3,"check total count of thread a results");
is($intervals_b->{total_count},scalar (grep { $_->{contact_id} == $custcontact9->{id} } values %customer_map),"check total count of thread b results");
is($intervals_b->{total_count},3,"check total count of thread b results");
#is($intervals_b->{total_count},scalar (grep { $_->{contact_id} == $custcontact9->{id} } values %customer_map),"check total count of thread b results");
#is($intervals_c->{total_count},3,"check total count of thread c results");
ok($t2 - $t1 < 2*$delay,'expected delay to assume only required contracts were locked');
ok($t2 - $t1 < $delta_serialized,'expected delay to assume only required contracts were locked and requests were performed in parallel');
} else {
diag('allow_delay_commit not set, skipping ...');
}
if (_get_allow_delay_commit()) {
my $custcontact1 = _create_customer_contact();
my $custcontact2 = _create_customer_contact();
_set_time(NGCP::Panel::Utils::DateTime::current_local->subtract(months => 3));
_create_customers_threaded(3,undef,undef,$custcontact9);
_create_customers_threaded(3,undef,undef,$custcontact3);
_create_customers_threaded(3,undef,undef,$custcontact1);
_create_customers_threaded(3,undef,undef,$custcontact2);
_set_time();
my $t1 = time;
my $delay = 10; #15;
my $delay = 5; #15;
my $t_a = threads->create(\&_fetch_intervals_worker,$delay,'id','asc',$custcontact3);
my $t_b = threads->create(\&_fetch_intervals_worker,$delay,'id','desc',$custcontact3);
my $t_a = threads->create(\&_fetch_intervals_worker,$delay,'id','asc',$custcontact2);
my $t_b = threads->create(\&_fetch_intervals_worker,$delay,'id','desc',$custcontact2);
#my $t_c = threads->create(\&_fetch_intervals_worker,$delay,'id','desc',$custcontact9);
my $intervals_a = $t_a->join();
my $intervals_b = $t_b->join();
@ -1036,12 +994,13 @@ if (_get_allow_fake_client_time() && $enable_profile_packages) {
if (!is_deeply($got_desc,[ reverse @{ $got_asc } ],'compare interval collection results of threaded requests deeply')) {
diag(Dumper({asc => $got_asc, desc => $got_desc}));
}
ok($t2 - $t1 > 2*$delay,'expected delay to assume balanceintervals requests were processed after another');
my $delta_serialized = $t2 - $t1;
ok($delta_serialized > 2*$delay,'expected delay to assume balanceintervals requests were processed after another');
#ok($t2 - $t1 < 3*$delay,'expected delay to assume only required contracts were locked');
$t1 = time;
$t_a = threads->create(\&_fetch_intervals_worker,$delay,'id','asc',$custcontact3);
$t_b = threads->create(\&_fetch_intervals_worker,$delay,'id','desc',$custcontact9);
$t_a = threads->create(\&_fetch_intervals_worker,$delay,'id','asc',$custcontact1);
$t_b = threads->create(\&_fetch_intervals_worker,$delay,'id','desc',$custcontact2);
#$t_c = threads->create(\&_fetch_intervals_worker,$delay,'id','desc',$custcontact3);
$intervals_a = $t_a->join();
$intervals_b = $t_b->join();
@ -1049,10 +1008,11 @@ if (_get_allow_fake_client_time() && $enable_profile_packages) {
$t2 = time;
is($intervals_a->{total_count},3,"check total count of thread a results");
is($intervals_b->{total_count},scalar (grep { $_->{contact_id} == $custcontact9->{id} } values %customer_map),"check total count of thread b results");
is($intervals_b->{total_count},3,"check total count of thread b results");
#is($intervals_b->{total_count},scalar (grep { $_->{contact_id} == $custcontact9->{id} } values %customer_map),"check total count of thread b results");
#is($intervals_c->{total_count},3,"check total count of thread c results");
ok($t2 - $t1 < 2*$delay,'expected delay to assume only required contracts were locked');
ok($t2 - $t1 < $delta_serialized,'expected delay to assume only required contracts were locked and requests were perfomed in parallel');
} else {
@ -1060,15 +1020,17 @@ if (_get_allow_fake_client_time() && $enable_profile_packages) {
}
if (_get_allow_delay_commit()) {
my $custcontact1 = _create_customer_contact();
my $custcontact2 = _create_customer_contact();
my $package = _create_profile_package('create','month',1,initial_balance => 1, carry_over_mode => 'discard', underrun_lock_threshold => 1, underrun_lock_level => 4);
_set_time(NGCP::Panel::Utils::DateTime::from_string('2015-05-17 13:00:00'));
_create_customers_threaded(3,2,$package,$custcontact9);
_create_customers_threaded(3,2,$package,$custcontact4);
_create_customers_threaded(3,2,$package,$custcontact1);
_create_customers_threaded(3,2,$package,$custcontact2);
my $t1 = time;
my $delay = 10.0; #15.0; #10.0; #2.0;
my $t_a = threads->create(\&_fetch_preferences_worker,$delay,'id','asc',$custcontact4);
my $t_b = threads->create(\&_fetch_preferences_worker,$delay,'id','desc',$custcontact4);
my $delay = 5.0; #15.0; #10.0; #2.0;
my $t_a = threads->create(\&_fetch_preferences_worker,$delay,'id','asc',$custcontact2);
my $t_b = threads->create(\&_fetch_preferences_worker,$delay,'id','desc',$custcontact2);
#my $t_c = threads->create(\&_fetch_preferences_worker,$delay,'id','desc',$custcontact9);
my $prefs_a = $t_a->join();
my $prefs_b = $t_b->join();
@ -1082,7 +1044,8 @@ if (_get_allow_fake_client_time() && $enable_profile_packages) {
if (!is_deeply($got_desc,[ reverse @{ $got_asc } ],'compare subscriber preference collection results of threaded requests deeply')) {
diag(Dumper({asc => $got_asc, desc => $got_desc}));
}
ok($t2 - $t1 > 2*$delay,'expected delay to assume subscriberpreferences requests were processed after another');
my $delta_serialized = $t2 - $t1;
ok($delta_serialized > 2*$delay,'expected delay to assume subscriberpreferences requests were processed after another');
#ok($t2 - $t1 < 3*$delay,'expected delay to assume only required contracts were locked');
for (my $i = 0; $i < 2*3; $i++) {
is($got_desc->[$i]->{lock},undef,"check if subscriber is unlocked initially");
@ -1114,8 +1077,8 @@ if (_get_allow_fake_client_time() && $enable_profile_packages) {
#}
$t1 = time;
$t_a = threads->create(\&_fetch_preferences_worker,$delay,'id','asc',$custcontact4);
$t_b = threads->create(\&_fetch_preferences_worker,$delay,'id','desc',$custcontact9);
$t_a = threads->create(\&_fetch_preferences_worker,$delay,'id','asc',$custcontact1);
$t_b = threads->create(\&_fetch_preferences_worker,$delay,'id','desc',$custcontact2);
#$t_c = threads->create(\&_fetch_preferences_worker,$delay,'id','desc',$custcontact4);
$prefs_a = $t_a->join();
$prefs_b = $t_b->join();
@ -1123,17 +1086,18 @@ if (_get_allow_fake_client_time() && $enable_profile_packages) {
$t2 = time;
is($prefs_a->{total_count},2*3,"check total count of thread a results");
is($prefs_b->{total_count},scalar (grep { $customer_map{$_->{customer_id}}->{contact_id} == $custcontact9->{id} } values %subscriber_map),"check total count of thread b results");
is($prefs_b->{total_count},2*3,"check total count of thread b results");
#is($prefs_b->{total_count},scalar (grep { $customer_map{$_->{customer_id}}->{contact_id} == $custcontact9->{id} } values %subscriber_map),"check total count of thread b results");
#is($prefs_c->{total_count},2*3,"check total count of thread c results");
$got_asc = $prefs_a->{_embedded}->{'ngcp:subscriberpreferences'};
for (my $i = 0; $i < 2*3; $i++) {
is($got_asc->[$i]->{lock},4,"check if subscriber is locked now");
}
ok($t2 - $t1 < 2*$delay,'expected delay to assume only required contracts were locked');
ok($t2 - $t1 < $delta_serialized,'expected delay to assume only required contracts were locked and requests were performed in parallel');
$t1 = time;
$t_a = threads->create(\&_fetch_preferences_worker,$delay,'id','asc',$custcontact4);
$t_a = threads->create(\&_fetch_preferences_worker,$delay,'id','asc',$custcontact2);
sleep($delay/2.0);
my $last_customer_id = shift(@{[sort {$b <=> $a} keys %customer_map]});
_check_interval_history($customer_map{$last_customer_id},[
@ -1152,15 +1116,17 @@ if (_get_allow_fake_client_time() && $enable_profile_packages) {
}
if (_get_allow_delay_commit()) {
my $custcontact1 = _create_customer_contact();
my $custcontact2 = _create_customer_contact();
my $package = _create_profile_package('create','month',1,initial_balance => 1, carry_over_mode => 'discard', underrun_lock_threshold => 1, underrun_lock_level => 4);
_set_time(NGCP::Panel::Utils::DateTime::from_string('2015-05-17 13:00:00'));
_create_customers_threaded(3,2,$package,$custcontact9);
_create_customers_threaded(3,2,$package,$custcontact5);
_create_customers_threaded(3,2,$package,$custcontact1);
_create_customers_threaded(3,2,$package,$custcontact2);
my $t1 = time;
my $delay = 10.0; #15.0; #10.0; #2.0;
my $t_a = threads->create(\&_fetch_subscribers_worker,$delay,'id','asc',$custcontact5);
my $t_b = threads->create(\&_fetch_subscribers_worker,$delay,'id','desc',$custcontact5);
my $delay = 5.0; #15.0; #10.0; #2.0;
my $t_a = threads->create(\&_fetch_subscribers_worker,$delay,'id','asc',$custcontact2);
my $t_b = threads->create(\&_fetch_subscribers_worker,$delay,'id','desc',$custcontact2);
#my $t_c = threads->create(\&_fetch_subscribers_worker,$delay,'id','desc',$custcontact9);
my $subs_a = $t_a->join();
my $subs_b = $t_b->join();
@ -1174,7 +1140,8 @@ if (_get_allow_fake_client_time() && $enable_profile_packages) {
if (!is_deeply($got_desc,[ reverse @{ $got_asc } ],'compare subscriber collection results of threaded requests deeply')) {
diag(Dumper({asc => $got_asc, desc => $got_desc}));
}
ok($t2 - $t1 > 2*$delay,'expected delay to assume subscribers requests were processed after another');
my $delta_serialized = $t2 - $t1;
ok($delta_serialized > 2*$delay,'expected delay to assume subscribers requests were processed after another');
#ok($t2 - $t1 < 3*$delay,'expected delay to assume only required contracts were locked');
for (my $i = 0; $i < 2*3; $i++) {
is($got_desc->[$i]->{lock},undef,"check if subscriber is unlocked initially");
@ -1206,8 +1173,8 @@ if (_get_allow_fake_client_time() && $enable_profile_packages) {
#}
$t1 = time;
$t_a = threads->create(\&_fetch_subscribers_worker,$delay,'id','asc',$custcontact5);
$t_b = threads->create(\&_fetch_subscribers_worker,$delay,'id','desc',$custcontact9);
$t_a = threads->create(\&_fetch_subscribers_worker,$delay,'id','asc',$custcontact1);
$t_b = threads->create(\&_fetch_subscribers_worker,$delay,'id','desc',$custcontact2);
#$t_c = threads->create(\&_fetch_subscribers_worker,$delay,'id','desc',$custcontact5);
$subs_a = $t_a->join();
$subs_b = $t_b->join();
@ -1215,16 +1182,17 @@ if (_get_allow_fake_client_time() && $enable_profile_packages) {
$t2 = time;
is($subs_a->{total_count},2*3,"check total count of thread a results");
is($subs_b->{total_count},scalar (grep { $customer_map{$_->{customer_id}}->{contact_id} == $custcontact9->{id} } values %subscriber_map),"check total count of thread b results");
is($subs_b->{total_count},2*3,"check total count of thread b results");
#is($subs_b->{total_count},scalar (grep { $customer_map{$_->{customer_id}}->{contact_id} == $custcontact9->{id} } values %subscriber_map),"check total count of thread b results");
#is($subs_c->{total_count},2*3,"check total count of thread c results");
$got_asc = $subs_a->{_embedded}->{'ngcp:subscribers'};
for (my $i = 0; $i < 2*3; $i++) {
is($got_asc->[$i]->{lock},4,"check if subscriber is locked now");
}
ok($t2 - $t1 < 2*$delay,'expected delay to assume only required contracts were locked');
ok($t2 - $t1 < $delta_serialized,'expected delay to assume only required contracts were locked and requests were performed in parallel');
$t1 = time;
$t_a = threads->create(\&_fetch_subscribers_worker,$delay,'id','asc',$custcontact5);
$t_a = threads->create(\&_fetch_subscribers_worker,$delay,'id','asc',$custcontact2);
sleep($delay/2.0);
my $last_customer_id = shift(@{[sort {$b <=> $a} keys %customer_map]});
_check_interval_history($customer_map{$last_customer_id},[
@ -1246,7 +1214,7 @@ if (_get_allow_fake_client_time() && $enable_profile_packages) {
diag('allow_fake_client_time not set, skipping ...');
}
for my $custcontact ($custcontact1,$custcontact2,$custcontact3,$custcontact4,$custcontact9) {
for my $custcontact (values %$customer_contact_map) { #$default_custcontact,$custcontact2,$custcontact3,$custcontact4,$custcontact9) {
{ #test balanceintervals root collection and item
_create_customers_threaded(3,undef,undef,$custcontact); # unless _get_allow_fake_client_time() && $enable_profile_packages;
@ -1517,6 +1485,29 @@ sub _create_customers_threaded {
diag('average time to create a customer: ' . ($t1 - $t0)/$number_of_customers);
}
sub _create_customer_contact {
my $n = (scalar keys %$customer_contact_map);
$req = HTTP::Request->new('POST', $uri.'/api/customercontacts/');
$req->header('Content-Type' => 'application/json');
$req->header('X-Request-Identifier' => $req_identifier) if $req_identifier;
$req->content(JSON::to_json({
firstname => "cust_contact_".$n."_first",
lastname => "cust_contact_".$n."_last",
email => "cust_contact".$n."\@custcontact.invalid",
reseller_id => $default_reseller_id,
}));
$res = $ua->request($req);
is($res->code, 201, "create customer contact $n");
$req = HTTP::Request->new('GET', $uri.'/'.$res->header('Location'));
$res = $ua->request($req);
is($res->code, 200, "fetch customer contact $n");
my $custcontact = JSON::from_json($res->decoded_content);
$customer_contact_map->{$custcontact->{id}} = $custcontact;
return $custcontact;
}
sub _create_customer {
my ($package,$record_label,$custcontact) = @_;
@ -1526,7 +1517,7 @@ sub _create_customer {
$req->header('X-Request-Identifier' => $req_identifier) if $req_identifier;
my $req_data = {
status => "active",
contact_id => (defined $custcontact ? $custcontact->{id} : $custcontact1->{id}),
contact_id => (defined $custcontact ? $custcontact->{id} : $default_custcontact->{id}),
type => "sipaccount",
($package ? (billing_profile_definition => 'package',
profile_package_id => $package->{id}) :

@ -17,7 +17,7 @@ BEGIN {
}
use NGCP::Panel::Utils::ProfilePackages qw();
my $enable_profile_packages = NGCP::Panel::Utils::ProfilePackages::ENABLE_PROFILE_PACKAGES;
#my $enable_profile_packages = NGCP::Panel::Utils::ProfilePackages::ENABLE_PROFILE_PACKAGES;
my $uri = $ENV{CATALYST_SERVER} || ('https://'.hostfqdn.':4443');
my ($netloc) = ($uri =~ m!^https?://(.*)/?.*$!);
@ -736,7 +736,7 @@ my @allcustomers = ();
}
if ($enable_profile_packages) {
{ #if ($enable_profile_packages) {
$req = HTTP::Request->new('POST', $uri.'/api/billingprofiles/');
$req->header('Content-Type' => 'application/json');

@ -16,7 +16,7 @@ BEGIN {
}
use NGCP::Panel::Utils::ProfilePackages qw();
my $enable_profile_packages = NGCP::Panel::Utils::ProfilePackages::ENABLE_PROFILE_PACKAGES;
#my $enable_profile_packages = NGCP::Panel::Utils::ProfilePackages::ENABLE_PROFILE_PACKAGES;
my $uri = $ENV{CATALYST_SERVER} || ('https://'.hostfqdn.':4443');
my ($netloc) = ($uri =~ m!^https?://(.*)/?.*$!);
@ -139,7 +139,7 @@ my $billingnetwork = JSON::from_json($res->decoded_content);
my %package_map = ();
if ($enable_profile_packages) {
{ #if ($enable_profile_packages) {
$req = HTTP::Request->new('POST', $uri.'/api/profilepackages/');
$req->header('Content-Type' => 'application/json');
$req->header('Prefer' => 'return=representation');

@ -0,0 +1,483 @@
use strict;
use warnings;
use Scalar::Util qw(looks_like_number);
#use Sipwise::Base; #causes segfault when creating threads..
use Net::Domain qw(hostfqdn);
use LWP::UserAgent;
use JSON qw();
use Test::More;
use DateTime::Format::Strptime;
use DateTime::Format::ISO8601;
use Data::Dumper;
use Storable;
use JSON::PP;
use LWP::Debug;
my $is_local_env = 0;
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 ($netloc) = ($uri =~ m!^https?://(.*)/?.*$!);
my ($ua, $req, $res);
$ua = LWP::UserAgent->new;
$ua->ssl_opts(
verify_hostname => 0,
SSL_verify_mode => 0,
);
my $user = $ENV{API_USER} // 'administrator';
my $pass = $ENV{API_PASS} // 'administrator';
$ua->credentials($netloc, "api_admin_http", $user, $pass);
my $t = time;
my $default_reseller_id = 1;
$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);
$req = HTTP::Request->new('POST', $uri.'/api/domains/');
$req->header('Content-Type' => 'application/json');
$req->content(JSON::to_json({
domain => 'test' . ($t-1) . '.example.org',
reseller_id => $default_reseller_id,
}));
$res = $ua->request($req);
is($res->code, 201, "POST test domain");
$req = HTTP::Request->new('GET', $uri.'/'.$res->header('Location'));
$res = $ua->request($req);
is($res->code, 200, "fetch POSTed test domain");
my $domain = JSON::from_json($res->decoded_content);
my $customer_map = {};
my $subscriber_map = {};
my $package_map = {};
my $voucher_map = {};
my $profile_map = {};
my $request_count = 0;
#goto SKIP;
{
my $profile = _create_billing_profile('PROFILE_1');
my $customer = _create_customer(billing_profile_definition => 'id',
billing_profile_id => $profile->{id},);
my $subscriber = _create_subscriber($customer);
my $voucher_1 = _create_voucher(10,'test1'.$t,$customer);
my $voucher_2 = _create_voucher(10,'test2'.$t,$customer,undef,valid_until => '2010-01-01 00:00:00');
my $request_token = $t."_".$request_count; $request_count++;
_perform_topup_cash({ id => 'invalid' },0.5,undef,$request_token,422);
_check_topup_log('failing topup cash validation (subscriber_id): ',[
{ outcome => 'failed', request_token => $request_token, message => 'Validation failed. field=\'subscriber_id\'' }
],'request_token='.$request_token);
$request_token = $t."_".$request_count; $request_count++;
_perform_topup_cash($subscriber,'invalid_amount',undef,$request_token,422);
_check_topup_log('failing topup cash validation (amount): ',[
{ outcome => 'failed', request_token => $request_token, message => 'Value cannot be converted to money' }
],'request_token='.$request_token);
$request_token = $t."_".$request_count; $request_count++;
_perform_topup_cash($subscriber,50,{ id => 'invalid' },$request_token,422);
_check_topup_log('failing topup cash validation (package_id): ',[
{ outcome => 'failed', request_token => $request_token, message => 'Validation failed. field=\'package_id\'' }
],'request_token='.$request_token);
$request_token = $t."_".$request_count; $request_count++;
_perform_topup_voucher($subscriber,{ code => 'invalid' },$request_token,422);
_check_topup_log('failing topup voucher validation (voucher code): ',[
{ outcome => 'failed', request_token => $request_token, message => 'Invalid voucher code \'invalid\'' }
],'request_token='.$request_token);
$request_token = $t."_".$request_count; $request_count++;
$request_token .= 'a' x (256 - length($request_token));
_perform_topup_voucher($subscriber,$voucher_1,$request_token,422);
_check_topup_log('failing topup voucher validation (request_token): ',[
{ outcome => 'failed', request_token => substr($request_token,0,255), message => 'Validation failed. field=\'request_token\'' }, #'Field should not exceed 255 characters' }
],'request_token='.substr($request_token,0,255));
$request_token = $t."_".$request_count; $request_count++;
_perform_topup_voucher($subscriber,$voucher_1,$request_token);
$request_token = $t."_".$request_count; $request_count++;
_perform_topup_voucher($subscriber,$voucher_1,$request_token,422);
_check_topup_log('failing topup voucher validation (voucher used): ',[
{ outcome => 'failed', request_token => $request_token, message => 'already used' }
],'request_token='.$request_token);
$request_token = $t."_".$request_count; $request_count++;
_perform_topup_voucher($subscriber,$voucher_2,$request_token,422);
_check_topup_log('failing topup voucher validation (voucher expired): ',[
{ outcome => 'failed', request_token => $request_token, message => 'expired' }
],'request_token='.$request_token);
}
SKIP:
{
my $profile_initial_1 = _create_billing_profile('INITIAL1');
my $profile_topup_1 = _create_billing_profile('TOPUP1');
#my $profile_underrun = _create_billing_profile('UNDERRUN');
my $package_1 = _create_profile_package('1st','month',1, initial_balance => 100,
#carry_over_mode => 'discard', underrun_lock_threshold => 50, underrun_lock_level => 4, underrun_profile_threshold => 50,
initial_profiles => [ { profile_id => $profile_initial_1->{id}, }, ],
topup_profiles => [ { profile_id => $profile_topup_1->{id}, }, ],
#underrun_profiles => [ { profile_id => $profile_underrun->{id}, }, ],
);
my $customer = _create_customer(billing_profile_definition => 'package',
profile_package_id => $package_1->{id},);
my $subscriber = _create_subscriber($customer);
my $voucher_1 = _create_voucher(10,'test3'.$t,$customer);
my $request_token_1 = $t."_".$request_count; $request_count++;
_perform_topup_cash($subscriber,0.5,undef,$request_token_1);
my $request_token_2 = $t."_".$request_count; $request_count++;
_perform_topup_voucher($subscriber,$voucher_1,$request_token_2);
_check_topup_log('successful topups - subscriber_id, outcome filter: ',[
{ outcome => 'ok', request_token => $request_token_1 },
{ outcome => 'ok', request_token => $request_token_2 },
],'subscriber_id='.$subscriber->{id}.'&outcome=ok');
_check_topup_log('successful topups - contract_id filter: ',[
{ outcome => 'ok', request_token => $request_token_1 },
{ outcome => 'ok', request_token => $request_token_2 },
],'contract_id='.$customer->{id});
my $profile_initial_2 = _create_billing_profile('INITIAL2');
my $profile_topup_2 = _create_billing_profile('TOPUP2');
#my $profile_underrun = _create_billing_profile('UNDERRUN');
my $package_2 = _create_profile_package('1st','month',1, initial_balance => 100,
#carry_over_mode => 'discard', underrun_lock_threshold => 50, underrun_lock_level => 4, underrun_profile_threshold => 50,
initial_profiles => [ { profile_id => $profile_initial_2->{id}, }, ],
topup_profiles => [ { profile_id => $profile_topup_2->{id}, }, ],
#underrun_profiles => [ { profile_id => $profile_underrun->{id}, }, ],
);
my $voucher_2 = _create_voucher(30,'test4'.$t,$customer,$package_2);
my $request_token_3 = $t."_".$request_count; $request_count++;
_perform_topup_voucher($subscriber,$voucher_2,$request_token_3);
_check_topup_log('successful topups - voucher_id filter: ',[
{ outcome => 'ok', request_token => $request_token_3 },
],'voucher_id='.$voucher_2->{id});
_check_topup_log('successful topups - amount_above filter: ',[
{ outcome => 'ok', request_token => $request_token_2 },
{ outcome => 'ok', request_token => $request_token_3 },
],'amount_above=1&subscriber_id='.$subscriber->{id});
_check_topup_log('successful topups - amount_below filter: ',[
{ outcome => 'ok', request_token => $request_token_1 },
{ outcome => 'ok', request_token => $request_token_2 },
],'amount_below=10&subscriber_id='.$subscriber->{id});
_check_topup_log('successful topups - timestamp_from filter: ',[
{ outcome => 'ok', request_token => $request_token_1 },
{ outcome => 'ok', request_token => $request_token_2 },
{ outcome => 'ok', request_token => $request_token_3 },
],'timestamp_from=2000-01-01T00:00:00&subscriber_id='.$subscriber->{id});
_check_topup_log('successful topups - balance before/after: ',[
{ outcome => 'ok', cash_balance_before => 1, cash_balance_after => 1.5, request_token => $request_token_1 },
{ outcome => 'ok', cash_balance_before => 1.5, cash_balance_after => 11.5, request_token => $request_token_2 },
{ outcome => 'ok', cash_balance_before => 11.5, cash_balance_after => 41.5, request_token => $request_token_3 },
],'contract_id='.$customer->{id});
_check_topup_log('successful topups - package before/after: ',[
{ outcome => 'ok', package_before_id=> $package_1->{id}, package_after_id=> $package_1->{id}, request_token => $request_token_1 },
{ outcome => 'ok', package_before_id=> $package_1->{id}, package_after_id=> $package_1->{id}, request_token => $request_token_2 },
{ outcome => 'ok', package_before_id=> $package_1->{id}, package_after_id=> $package_2->{id}, request_token => $request_token_3 },
],'contract_id='.$customer->{id});
_check_topup_log('successful topups - profile before/after: ',[
{ outcome => 'ok', profile_before_id => $profile_initial_1->{id}, profile_after_id => $profile_topup_1->{id}, request_token => $request_token_1 },
{ outcome => 'ok', profile_before_id => $profile_topup_1->{id}, profile_after_id => $profile_topup_1->{id}, request_token => $request_token_2 },
{ outcome => 'ok', profile_before_id => $profile_topup_1->{id}, profile_after_id => $profile_topup_2->{id},request_token => $request_token_3 },
],'contract_id='.$customer->{id});
}
done_testing;
sub _check_topup_log {
my ($label,$expected_topup_log,$filter_query) = @_;
my $total_count = (scalar @$expected_topup_log);
my $i = 0;
my $nexturi = $uri.'/api/topuplogs/?page=1&rows=10&order_by_direction=asc&order_by=timestamp'.(defined $filter_query ? '&'.$filter_query : '');
do {
$req = HTTP::Request->new('GET',$nexturi);
$res = $ua->request($req);
#$res = $ua->get($nexturi);
is($res->code, 200, $label."fetch topup log 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->{_embedded}->{'ngcp:topuplogs'} eq "ARRAY", $label."check if 'ngcp:topuplogs' is array");
#my $page_items = {};
foreach my $log_record (@{ $collection->{_embedded}->{'ngcp:topuplogs'} }) {
#$req = HTTP::Request->new('GET',$uri.$log_record->{_links}->{self}->{href});
#$res = $ua->request($req);
#is($res->code, 200, $label."fetch topup log entry");
#my $got = JSON::from_json($res->decoded_content);
#is_deeply($got,$log_record,$label.'check topup log entry deeply');
_compare_log_record($label,$log_record,$expected_topup_log->[$i]);
$i++
}
} while($nexturi);
ok($i == $total_count,$label."check if all expected items are listed");
}
sub _compare_log_record {
my ($label,$got,$expected) = @_;
foreach my $field (keys %$expected) {
if ('message' eq $field) {
ok($got->{$field} =~ /$expected->{$field}/,$label."check log '" . $field . "': " . $got->{$field} . " =~ /" . $expected->{$field} . '/');
} else {
is($got->{$field},$expected->{$field},$label."check log '" . $field . "': " . $got->{$field} . " = " . $expected->{$field});
}
}
}
sub _create_customer {
my (@further_opts) = @_;
$req = HTTP::Request->new('POST', $uri.'/api/customers/');
$req->header('Content-Type' => 'application/json');
my $req_data = {
status => "active",
contact_id => $custcontact->{id},
type => "sipaccount",
max_subscribers => undef,
external_id => undef,
@further_opts
};
$req->content(JSON::to_json($req_data));
$res = $ua->request($req);
is($res->code, 201, "create test customer");
my $request = $req;
$req = HTTP::Request->new('GET', $uri.'/'.$res->header('Location'));
$res = $ua->request($req);
is($res->code, 200, "fetch test customer");
my $customer = JSON::from_json($res->decoded_content);
$customer_map->{$customer->{id}} = $customer;
return $customer;
}
sub _create_profile_package {
my ($start_mode,$interval_unit,$interval_value,@further_opts) = @_; #$notopup_discard_intervals
$req = HTTP::Request->new('POST', $uri.'/api/profilepackages/');
$req->header('Content-Type' => 'application/json');
$req->header('Prefer' => 'return=representation');
my $name = $start_mode . ($interval_unit ? '/' . $interval_value . ' ' . $interval_unit : '');
$req->content(JSON::to_json({
name => "test '" . $name . "' profile package " . (scalar keys %$package_map) . '_' . $t,
#description => "test prof package descr " . (scalar keys %$package_map) . '_' . $t,
description => $start_mode . "/" . $interval_value . " " . $interval_unit . "s",
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,) : ()),
#notopup_discard_intervals => $notopup_discard_intervals,
@further_opts,
}));
$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);
$res = $ua->request($req);
is($res->code, 200, "fetch POSTed profilepackage - '" . $name . "'");
my $package = JSON::from_json($res->decoded_content);
$package_map->{$package->{id}} = $package;
return $package;
}
sub _create_voucher {
my ($amount,$code,$customer,$package,@further_opts) = @_;
my $dtf = DateTime::Format::Strptime->new(
pattern => '%F %T',
);
$req = HTTP::Request->new('POST', $uri.'/api/vouchers/');
$req->header('Content-Type' => 'application/json');
my $req_data = {
amount => $amount * 100.0,
code => $code,
customer_id => ($customer ? $customer->{id} : undef),
package_id => ($package ? $package->{id} : undef),
reseller_id => $default_reseller_id,
valid_until => '2037-01-01 00:00:00',
@further_opts,
#valid_until => $dtf->format_datetime($valid_until_dt ? $valid_until_dt : NGCP::Panel::Utils::DateTime::current_local->add(years => 1)),
};
$req->content(JSON::to_json($req_data));
$res = $ua->request($req);
my $label = 'test voucher (' . ($customer ? 'for customer ' . $customer->{id} : 'no customer') . ', ' . ($package ? 'for package ' . $package->{id} : 'no package') . ')';
is($res->code, 201, "create " . $label);
my $request = $req;
$req = HTTP::Request->new('GET', $uri.'/'.$res->header('Location'));
$res = $ua->request($req);
is($res->code, 200, "fetch " . $label);
my $voucher = JSON::from_json($res->decoded_content);
$voucher_map->{$voucher->{id}} = $voucher;
return $voucher;
}
sub _create_subscriber {
my ($customer) = @_;
$req = HTTP::Request->new('POST', $uri.'/api/subscribers/');
$req->header('Content-Type' => 'application/json');
my $req_data = {
domain_id => $domain->{id},
username => 'cust_subscriber_' . (scalar keys %$subscriber_map) . '_'.$t,
password => 'cust_subscriber_password',
customer_id => $customer->{id},
#status => "active",
};
$req->content(JSON::to_json($req_data));
$res = $ua->request($req);
is($res->code, 201, "POST test subscriber");
my $request = $req;
$req = HTTP::Request->new('GET', $uri.'/'.$res->header('Location'));
$res = $ua->request($req);
is($res->code, 200, "fetch POSTed test subscriber");
my $subscriber = JSON::from_json($res->decoded_content);
$subscriber_map->{$subscriber->{id}} = $subscriber;
return $subscriber;
}
sub _perform_topup_voucher {
my ($subscriber,$voucher,$request_token,$error_code) = @_;
$req = HTTP::Request->new('POST', $uri.'/api/topupvouchers/');
$req->header('Content-Type' => 'application/json');
my $req_data = {
code => $voucher->{code},
subscriber_id => $subscriber->{id},
(defined $request_token ? (request_token => $request_token) : ()),
};
$req->content(JSON::to_json($req_data));
$res = $ua->request($req);
$error_code //= 204;
is($res->code, $error_code, ($error_code == 204 ? 'perform' : 'attempt')." perform topup with voucher " . $voucher->{code});
}
sub _perform_topup_cash {
my ($subscriber,$amount,$package,$request_token,$error_code) = @_;
$req = HTTP::Request->new('POST', $uri.'/api/topupcash/');
$req->header('Content-Type' => 'application/json');
my $req_data = {
amount => ( looks_like_number($amount) ? $amount * 100.0 : $amount),
package_id => ($package ? $package->{id} : undef),
subscriber_id => $subscriber->{id},
(defined $request_token ? (request_token => $request_token) : ()),
};
$req->content(JSON::to_json($req_data));
$res = $ua->request($req);
$error_code //= 204;
is($res->code, $error_code, ($error_code == 204 ? 'perform' : 'attempt')." topup with amount " . ( looks_like_number($amount) ? $amount * 100.0 . ' cents' : $amount) . ", " . ($package ? 'package id ' . $package->{id} : 'no package'));
}
sub _create_billing_profile {
my ($name) = @_;
$req = HTTP::Request->new('POST', $uri.'/api/billingprofiles/');
$req->header('Content-Type' => 'application/json');
$req->header('Prefer' => 'return=representation');
my $req_data = {
name => $name." $t",
handle => $name."_$t",
reseller_id => $default_reseller_id,
};
$req->content(JSON::to_json($req_data));
$res = $ua->request($req);
is($res->code, 201, "POST test billing profile " . $name);
my $request = $req;
$req = HTTP::Request->new('GET', $uri.'/'.$res->header('Location'));
$res = $ua->request($req);
is($res->code, 200, "fetch POSTed billing profile " . $name);
my $billingprofile = JSON::from_json($res->decoded_content);
$profile_map->{$billingprofile->{id}} = $billingprofile;
return $billingprofile;
}
Loading…
Cancel
Save