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: I86eb845f6173803705b12cc7e5cdbac9a3153a0achanges/19/2819/6
parent
59dcde8cea
commit
7f7e3332d1
@ -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:
|
@ -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;
|
@ -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;
|
@ -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…
Reference in new issue