MT#17263 edit offpeak/onpeak using api

+input validation for verlapping time ranges
 for both weekdays and special. can be disabled.
+root entiy locking
+peektime special is growing and not paginated,
 so PUT/PATCH will get slower if special peektimes
 are added on a regular basis. they can be removed
 using the UI however
+testcase to check overlap detection

Change-Id: I935d943078ab5c81263da88ecd04e004deb26c8b
changes/78/4078/6
Rene Krenn 10 years ago
parent 81c4dde54f
commit ad3719772a

@ -11,6 +11,7 @@ use HTTP::Status qw(:constants);
use MooseX::ClassAttribute qw(class_has);
use NGCP::Panel::Utils::DateTime;
use NGCP::Panel::Utils::Reseller qw();
use NGCP::Panel::Utils::Billing qw();
use Path::Tiny qw(path);
BEGIN { extends 'Catalyst::Controller::ActionRole'; }
require Catalyst::ActionRole::ACL;
@ -21,7 +22,7 @@ require Catalyst::ActionRole::RequireSSL;
class_has 'api_description' => (
is => 'ro',
isa => 'Str',
default =>
default =>
'Defines a collection of <a href="#billingfees">Billing Fees</a> and <a href="#billingzones">Billing Zones</a> and can be assigned to <a href="#customers">Customers</a> and <a href="#contracts">System Contracts</a>.'
);
@ -121,7 +122,7 @@ sub GET :Allow {
$hal->resource({
total_count => $total_count,
});
my $response = HTTP::Response->new(HTTP_OK, undef,
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);
@ -156,7 +157,7 @@ sub POST :Allow {
{
my $schema = $c->model('DB');
my $resource = $self->get_valid_post_data(
c => $c,
c => $c,
media_type => 'application/json',
);
last unless $resource;
@ -180,16 +181,42 @@ sub POST :Allow {
my ($err) = @_;
$self->error($c, HTTP_UNPROCESSABLE_ENTITY, $err);
});
my $weekday_peaktimes_to_create = [];
last unless NGCP::Panel::Utils::Billing::prepare_peaktime_weekdays(c => $c,
resource => $resource,
peaktimes_to_create => $weekday_peaktimes_to_create,
err_code => sub {
my ($err) = @_;
$self->error($c, HTTP_UNPROCESSABLE_ENTITY, $err);
}
);
my $special_peaktimes_to_create = [];
last unless NGCP::Panel::Utils::Billing::prepare_peaktime_specials(c => $c,
resource => $resource,
peaktimes_to_create => $special_peaktimes_to_create,
err_code => sub {
my ($err) = @_;
$self->error($c, HTTP_UNPROCESSABLE_ENTITY, $err);
}
);
my $billing_profile;
try {
$billing_profile= $schema->resultset('billing_profiles')->create($resource);
foreach my $weekday_peaktime (@$weekday_peaktimes_to_create) {
$billing_profile->billing_peaktime_weekdays->create($weekday_peaktime);
}
foreach my $special_peaktime (@$special_peaktimes_to_create) {
$billing_profile->billing_peaktime_specials->create($special_peaktime);
}
} catch($e) {
$c->log->error("failed to create billing profile: $e"); # TODO: user, message, trace, ...
$self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Failed to create billing profile.");
last;
}
last unless $self->add_create_journal_item_hal($c,sub {
my $self = shift;
my ($c) = @_;

@ -37,7 +37,7 @@ __PACKAGE__->config(
ACLDetachTo => '/api/root/invalid_user',
AllowedRole => [qw/admin reseller/],
Does => [qw(ACL RequireSSL)],
}) }
}) }
},
action_roles => [qw(HTTPMethods)],
);
@ -95,12 +95,13 @@ sub OPTIONS :Allow {
sub PATCH :Allow {
my ($self, $c, $id) = @_;
my $guard = $c->model('DB')->txn_scope_guard;
$c->model('DB')->set_transaction_isolation('READ COMMITTED');
{
my $preference = $self->require_preference($c);
last unless $preference;
my $json = $self->get_valid_patch_data(
c => $c,
c => $c,
id => $id,
media_type => 'application/json-patch+json',
);
@ -115,7 +116,7 @@ sub PATCH :Allow {
my $form = $self->get_form($c);
$profile = $self->update_profile($c, $profile, $old_resource, $resource, $form);
last unless $profile;
my $hal = $self->hal_from_profile($c, $profile, $form);
last unless $self->add_update_journal_item_hal($c,$hal);
@ -141,6 +142,7 @@ sub PATCH :Allow {
sub PUT :Allow {
my ($self, $c, $id) = @_;
my $guard = $c->model('DB')->txn_scope_guard;
$c->model('DB')->set_transaction_isolation('READ COMMITTED');
{
my $preference = $self->require_preference($c);
last unless $preference;
@ -161,7 +163,7 @@ sub PUT :Allow {
my $hal = $self->hal_from_profile($c, $profile, $form);
last unless $self->add_update_journal_item_hal($c,$hal);
$guard->commit;
if ('minimal' eq $preference) {
@ -187,7 +189,7 @@ sub item_base_journal :Journal {
my $self = shift @_;
return $self->handle_item_base_journal(@_);
}
sub journals_get :Journal {
my $self = shift @_;
return $self->handle_journals_get(@_);

@ -778,15 +778,7 @@ sub peaktime_weekdays_edit :Chained('peaktime_weekdays_base') :PathPart('edit')
sub load_weekdays {
my ($self, $c) = @_;
my @WEEKDAYS = (
$c->loc('Monday'),
$c->loc('Tuesday'),
$c->loc('Wednesday'),
$c->loc('Thursday'),
$c->loc('Friday'),
$c->loc('Saturday'),
$c->loc('Sunday')
);
my @WEEKDAYS = @{NGCP::Panel::Utils::DateTime::get_weekday_names($c)};
my @weekdays;
for(0 .. 6) {

@ -13,7 +13,7 @@ has_field 'weekday' => (
type => 'Hidden',
);
has_field 'start' => (
has_field 'start' => (
type => 'Text',
do_label => 0,
do_wrapper => 1,
@ -60,7 +60,7 @@ sub validate {
|| $parsetime2->parse_datetime($etime);
if ($end < $start) {
my $err_msg = 'Start time must be later than end time.';
my $err_msg = 'End time must be later than start time.';
$self->field('start')->add_error($err_msg);
$self->field('end')->add_error($err_msg);
}

@ -0,0 +1,65 @@
package NGCP::Panel::Form::BillingProfile::PeaktimeAPI;
use HTML::FormHandler::Moose;
extends 'NGCP::Panel::Form::BillingProfile::Admin';
has_field 'peaktime_weekdays' => (
type => 'Repeatable',
element_attr => {
rel => ['tooltip'],
title => ['The \'weekday\' peak-time schedule for this billing profile. It is represented by an array of objects, each containing the keys "weekday" (0 .. Monday, 6 .. Sunday), "start" (HH:mm:ss) and "stop" (HH:mm:ss). Each time range provided determines when to use a fee\'s offpeak rates.']
},
);
has_field 'peaktime_weekdays.id' => (
type => 'Hidden',
);
has_field 'peaktime_weekdays.weekday' => (
type => 'Integer',
required => 1,
);
has_field 'peaktime_weekdays.start' => (
type => 'Text',
required => 0,
);
has_field 'peaktime_weekdays.stop' => (
type => 'Text',
required => 0,
);
has_field 'peaktime_special' => (
type => 'Repeatable',
element_attr => {
rel => ['tooltip'],
title => ['The \'special\' peak-time schedule for this billing profile. It is represented by an array of objects, each containing the keys "start" (YYYY-MM-DD HH:mm:ss) and "stop" (YYYY-MM-DD HH:mm:ss). Each time range provided determines when to use a fee\'s offpeak rates.']
},
);
has_field 'peaktime_special.id' => (
type => 'Hidden',
);
has_field 'peaktime_special.start' => (
type => '+NGCP::Panel::Field::DateTime',
required => 1,
);
has_field 'peaktime_special.stop' => (
type => '+NGCP::Panel::Field::DateTime',
required => 1,
);
has_block 'fields' => (
tag => 'div',
class => [qw/modal-body/],
render_list => [qw/reseller handle name prepaid interval_charge interval_free_time interval_free_cash
fraud_interval_limit fraud_interval_lock fraud_interval_notify
fraud_daily_limit fraud_daily_lock fraud_daily_notify fraud_use_reseller_rates
currency id
status peaktime_weekdays peaktime_special/],
);
1;

@ -15,7 +15,7 @@ use HTTP::Status qw(:constants);
use NGCP::Panel::Utils::DateTime;
use NGCP::Panel::Utils::Reseller qw();
use NGCP::Panel::Utils::Contract;
use NGCP::Panel::Form::BillingProfile::Admin qw();
use NGCP::Panel::Form::BillingProfile::PeaktimeAPI qw();
use NGCP::Panel::Utils::Billing qw();
sub item_rs {
@ -43,7 +43,7 @@ sub item_rs {
sub get_form {
my ($self, $c) = @_;
return NGCP::Panel::Form::BillingProfile::Admin->new;
return NGCP::Panel::Form::BillingProfile::PeaktimeAPI->new;
}
sub hal_from_profile {
@ -51,6 +51,9 @@ sub hal_from_profile {
my %resource = $profile->get_inflated_columns;
my $weekday_peaktimes = NGCP::Panel::Utils::Billing::resource_from_peaktime_weekdays($profile);
my $special_peaktimes = NGCP::Panel::Utils::Billing::resource_from_peaktime_specials($profile);
# TODO: we should return the fees in an embedded field,
# if the structure is returned for one single item
# (make it a method flag)
@ -82,6 +85,8 @@ sub hal_from_profile {
);
$resource{id} = int($profile->id);
$resource{peaktime_weekdays} = $weekday_peaktimes;
$resource{peaktime_special} = $special_peaktimes;
$hal->resource({%resource});
return $hal;
}
@ -93,6 +98,13 @@ sub profile_by_id {
return $profiles->find($id);
}
sub lock_profile {
my ($self,$c,$profile_id) = @_;
return $c->model('DB')->resultset('billing_profiles')->find({
id => $profile_id
},{for => 'update'});
}
sub update_profile {
my ($self, $c, $profile, $old_resource, $resource, $form) = @_;
@ -120,10 +132,39 @@ sub update_profile {
$self->error($c, HTTP_UNPROCESSABLE_ENTITY, $err);
});
my $weekday_peaktimes_to_create = [];
return unless NGCP::Panel::Utils::Billing::prepare_peaktime_weekdays(c => $c,
resource => $resource,
peaktimes_to_create => $weekday_peaktimes_to_create,
err_code => sub {
my ($err) = @_;
$self->error($c, HTTP_UNPROCESSABLE_ENTITY, $err);
}
);
my $special_peaktimes_to_create = [];
return unless NGCP::Panel::Utils::Billing::prepare_peaktime_specials(c => $c,
resource => $resource,
peaktimes_to_create => $special_peaktimes_to_create,
err_code => sub {
my ($err) = @_;
$self->error($c, HTTP_UNPROCESSABLE_ENTITY, $err);
}
);
my $old_prepaid = $profile->prepaid;
try {
$profile = $self->lock_profile($c,$profile->id);
$profile->update($resource);
$profile->billing_peaktime_weekdays->delete;
foreach my $weekday_peaktime (@$weekday_peaktimes_to_create) {
$profile->billing_peaktime_weekdays->create($weekday_peaktime);
}
$profile->billing_peaktime_specials->delete;
foreach my $special_peaktime (@$special_peaktimes_to_create) {
$profile->billing_peaktime_specials->create($special_peaktime);
}
NGCP::Panel::Utils::Billing::switch_prepaid(c => $c,
profile_id => $profile->id,
old_prepaid => $old_prepaid,
@ -135,7 +176,7 @@ sub update_profile {
$self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Internal Server Error.");
return;
};
return $profile;
}

@ -48,35 +48,11 @@ sub hal_from_contract {
my $billing_profile_id = $billing_mapping->billing_profile_id;
my $future_billing_profiles = NGCP::Panel::Utils::Contract::resource_from_future_mappings($contract);
my $billing_profiles = NGCP::Panel::Utils::Contract::resource_from_mappings($contract);
#my $stime = NGCP::Panel::Utils::DateTime::current_local()->truncate(to => 'month');
#my $etime = $stime->clone->add(months => 1);
#my $contract_balance = $contract->contract_balances
# ->find({
# start => { '>=' => $stime },
# end => { '<' => $etime },
# });
#unless($contract_balance) {
# try {
# NGCP::Panel::Utils::Contract::create_contract_balance(
# c => $c,
# profile => $billing_mapping->billing_profile,
# contract => $contract,
# );
# } catch($e) {
# $c->log->error("Failed to create current contract balance for contract id '".$contract->id."': $e");
# $self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Internal Server Error.");
# return;
# }
# $contract_balance = $contract->contract_balances->find({
# start => { '>=' => $stime },
# end => { '<' => $etime },
# });
#}
#we leave this here to keep the former behaviour: contract balances are also created upon GET api/contracts/4711
NGCP::Panel::Utils::ProfilePackages::catchup_contract_balances(c => $c,
contract => $contract,
now => $now);
now => $now);
my %resource = $contract->get_inflated_columns;
@ -88,7 +64,7 @@ sub hal_from_contract {
push(@profile_links,Data::HAL::Link->new(relation => 'ngcp:billingnetworks', href => sprintf("/api/billingnetworks/%d", $mapping->network_id)));
}
}
my $hal = Data::HAL->new(
links => [
Data::HAL::Link->new(
@ -118,7 +94,7 @@ sub hal_from_contract {
exceptions => [ "contact_id", "billing_profile_id" ],
);
$resource{type} = $billing_mapping->product->class;
$resource{type} = $billing_mapping->product->class;
$resource{billing_profiles} = $future_billing_profiles;
$resource{all_billing_profiles} = $billing_profiles;
@ -140,9 +116,9 @@ sub update_contract {
my $billing_mapping = $contract->billing_mappings->find($contract->get_column('bmid'));
my $billing_profile = $billing_mapping->billing_profile;
my $old_package = $contract->profile_package;
my $old_package = $contract->profile_package;
$form //= $self->get_form($c);
# TODO: for some reason, formhandler lets missing contact_id slip thru
$resource->{contact_id} //= undef;
@ -155,7 +131,7 @@ sub update_contract {
);
#my $now = NGCP::Panel::Utils::DateTime::current_local;
my $mappings_to_create = [];
my $delete_mappings = 0;
my $set_package = ($resource->{billing_profile_definition} // 'id') eq 'package';
@ -192,10 +168,10 @@ sub update_contract {
$contract->update($resource);
NGCP::Panel::Utils::Contract::remove_future_billing_mappings($contract,$now) if $delete_mappings;
foreach my $mapping (@$mappings_to_create) {
$contract->billing_mappings->create($mapping);
$contract->billing_mappings->create($mapping);
}
$contract = $self->contract_by_id($c, $contract->id,1,$now);
my $balance = NGCP::Panel::Utils::ProfilePackages::catchup_contract_balances(c => $c,
contract => $contract,
old_package => $old_package,
@ -206,11 +182,11 @@ sub update_contract {
balance => $balance,
now => $now,
profiles_added => ($set_package ? scalar @$mappings_to_create : 0),
);
);
$billing_mapping = $contract->billing_mappings->find($contract->get_column('bmid'));
$billing_profile = $billing_mapping->billing_profile;
$billing_profile = $billing_mapping->billing_profile;
if($old_resource->{status} ne $resource->{status}) {
if($contract->id == 1) {
$self->error($c, HTTP_FORBIDDEN, "Cannot set contract status to '".$resource->{status}."' for contract id '1'");
@ -221,13 +197,13 @@ sub update_contract {
contract => $contract,
);
}
# TODO: what about changed product, do we allow it?
} catch($e) {
$c->log->error("Failed to update contract id '".$contract->id."': $e");
$self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Internal Server Error.");
return;
};
};
return $contract;
}

@ -6,23 +6,30 @@ use Text::CSV_XS;
use IO::String;
use NGCP::Schema;
use NGCP::Panel::Utils::Preferences qw();
use NGCP::Panel::Utils::DateTime;
use DateTime::Format::Strptime qw();
use NGCP::Panel::Utils::IntervalTree::Simple;
use constant _CHECK_PEAKTIME_WEEKDAY_OVERLAPS => 1;
use constant _CHECK_PEAKTIME_SPECIALS_OVERLAPS => 1;
sub check_profile_update_item {
my ($c,$new_resource,$old_item,$err_code) = @_;
return 1 unless $old_item;
if (!defined $err_code || ref $err_code ne 'CODE') {
$err_code = sub { return 0; };
}
if ($old_item->status eq 'terminated') {
return 0 unless &{$err_code}("Billing profile is already terminated and cannot be changed.",'status');
}
my $contract_cnt = $old_item->get_column('contract_cnt');
#my $package_cnt = $old_item->get_column('package_cnt');
if (($contract_cnt > 0)
&& defined $new_resource->{interval_charge} && $old_item->interval_charge != $new_resource->{interval_charge}) {
return 0 unless &{$err_code}("Interval charge cannot be changed (profile linked to $contract_cnt contracts).",'interval_charge');
@ -40,6 +47,143 @@ sub check_profile_update_item {
}
sub prepare_peaktime_weekdays {
my(%params) = @_;
my ($c,$resource,$err_code,$peaktimes_to_create) = @params{qw/c resource err_code peaktimes_to_create/};
if (!defined $err_code || ref $err_code ne 'CODE') {
$err_code = sub { return 0; };
}
my $peaktime_weekdays = delete $resource->{peaktime_weekdays};
$peaktime_weekdays //= [];
if ('ARRAY' ne ref $peaktime_weekdays) {
return 0 unless &{$err_code}("peaktime_weekdays is not an array");
}
my @WEEKDAYS = @{NGCP::Panel::Utils::DateTime::get_weekday_names($c)};
my %intersecter_map = ();
foreach my $peaktime_weekday (@$peaktime_weekdays) {
if ($peaktime_weekday->{weekday} < 0 || $peaktime_weekday->{weekday} > 6) {
return 0 unless &{$err_code}("Peaktime weekday must be between 0 (Monday) and 6 (Sunday)");
}
my $weekday = $peaktime_weekday->{weekday};
my $parsetime = DateTime::Format::Strptime->new(pattern => '%T');
my $parsetime2 = DateTime::Format::Strptime->new(pattern => '%R');
my $stime = $peaktime_weekday->{start};
my $etime = $peaktime_weekday->{stop};
$stime = '00:00:00' unless($stime && length($stime));
$etime = '23:59:59' unless($etime && length($etime));
my $start = $parsetime->parse_datetime($stime)
|| $parsetime2->parse_datetime($stime);
my $end = $parsetime->parse_datetime($etime)
|| $parsetime2->parse_datetime($etime);
unless ($start) {
return 0 unless &{$err_code}("Unknown weekday peaktime start time '$stime'.");
}
unless ($end) {
return 0 unless &{$err_code}("Unknown weekday peaktime stop time '$etime'.");
}
$stime = $parsetime->format_datetime($start);
$etime = $parsetime->format_datetime($end);
if ($end < $start) { #<= actually
return 0 unless &{$err_code}("Peaktime ($weekday - $WEEKDAYS[$weekday]) end time $etime must be later than start time $stime.");
}
my $intersecter;
if (_CHECK_PEAKTIME_WEEKDAY_OVERLAPS) {
if (exists $intersecter_map{$weekday}) {
$intersecter = $intersecter_map{$weekday};
} else {
$intersecter = NGCP::Panel::Utils::IntervalTree::Simple->new();
$intersecter_map{$weekday} = $intersecter;
}
}
if (defined $intersecter) {
my $from = $start->hour * 3600 + $start->minute * 60 + $start->second;
my $to = $end->hour * 3600 + $end->minute * 60 + $end->second + 1;
my $label = '(' . $weekday . ' - ' . $WEEKDAYS[$weekday] . ' ' . $stime . ' - ' . $etime .')';
my $overlaps_with = $intersecter->find($from,$to);
if ((scalar @$overlaps_with) > 0) {
return 0 unless &{$err_code}("Peaktime $label overlaps with peaktimes " . join(", ",@$overlaps_with));
} else {
$intersecter->insert($from,$to,$label);
}
}
if ('ARRAY' eq ref $peaktimes_to_create) {
push(@$peaktimes_to_create,{ weekday => $weekday,
start => $stime,
end => $etime,
});
}
}
return 1;
}
sub prepare_peaktime_specials {
my(%params) = @_;
my ($c,$resource,$err_code,$peaktimes_to_create) = @params{qw/c resource err_code peaktimes_to_create/};
if (!defined $err_code || ref $err_code ne 'CODE') {
$err_code = sub { return 0; };
}
my $peaktime_specials = delete $resource->{peaktime_special};
$peaktime_specials //= [];
if ('ARRAY' ne ref $peaktime_specials) {
return 0 unless &{$err_code}("peaktime_special is not an array");
}
my $intersecter = (_CHECK_PEAKTIME_SPECIALS_OVERLAPS ? NGCP::Panel::Utils::IntervalTree::Simple->new() : undef);
foreach my $peaktime_special (@$peaktime_specials) {
my $stime = $peaktime_special->{start};
my $etime = $peaktime_special->{stop};
#format checked by form
my $start = (defined $stime ? NGCP::Panel::Utils::DateTime::from_string($stime) : undef);
my $end = (defined $etime ? NGCP::Panel::Utils::DateTime::from_string($etime) : undef);
#although nullable, rateomat logic does not support open intervals
unless ($start) {
return 0 unless &{$err_code}("Empty special peaktime start timestamp.");
}
unless ($end) {
return 0 unless &{$err_code}("Empty special peaktime stop timestamp.");
}
if ($end < $start) { #<= actually
return 0 unless &{$err_code}("Special peaktime end timestamp $etime must be later than start timestamp $stime.");
}
if (defined $intersecter) {
my $from = $start->epoch;
my $to = $end->epoch + 1;
my $label = $stime . ' - ' . $etime;
my $overlaps_with = $intersecter->find($from,$to);
if ((scalar @$overlaps_with) > 0) {
return 0 unless &{$err_code}("Special peaktime $label overlaps with peaktimes " . join(", ",@$overlaps_with));
} else {
$intersecter->insert($from,$to,$label);
}
}
if ('ARRAY' eq ref $peaktimes_to_create) {
push(@$peaktimes_to_create,{
start => $start,
end => $end,
});
}
}
return 1;
}
sub process_billing_fees{
my(%params) = @_;
my ($c,$data,$profile,$schema) = @params{qw/c data profile schema/};
@ -87,9 +231,9 @@ sub process_billing_fees{
c => $c,
schema => $schema,
profile => $profile,
fees => \@fees
fees => \@fees
);
my $text = $c->loc('Billing Fee successfully uploaded');
if(@fails) {
$text .= $c->loc(", but skipped the following line numbers: ") . (join ", ", @fails);
@ -249,13 +393,13 @@ sub switch_prepaid {
my $rs = $schema->resultset('billing_mappings')->search({
billing_profile_id => $profile_id,
});
if($old_prepaid && !$new_prepaid ||
!$old_prepaid && $new_prepaid) {
#this will taking too long, prohibit it:
#this will taking too long, prohibit it:
#die("changing the prepaid flag is not allowed");
foreach my $mapping ($rs->all) {
my $contract = $mapping->contract;
next unless($contract->contact->reseller_id); # skip non-customers
@ -268,11 +412,11 @@ sub switch_prepaid {
prov_subscriber => $prov_sub,
value => ($new_prepaid ? 1 : 0),
attribute => 'prepaid'
);
);
}
}
}
}
sub get_contract_count_stmt {
@ -293,6 +437,50 @@ sub get_datatable_cols {
}
sub resource_from_peaktime_weekdays {
my ($profile) = @_;
my $datetime_fmt = DateTime::Format::Strptime->new(
pattern => '%T',
);
my $rs = $profile->billing_peaktime_weekdays->search_rs(
undef,
{ order_by => { '-asc' => [ 'weekday', 'start', 'id' ]},
});
my @weekday_peaktimes = ();
foreach my $weekday_peaktime ($rs->all) {
my %wp = ( weekday => $weekday_peaktime->weekday );
$wp{start} = $weekday_peaktime->start; #($weekday_peaktime->start ? $datetime_fmt->format_datetime($weekday_peaktime->start) : undef);
$wp{stop} = $weekday_peaktime->end; #($weekday_peaktime->end ? $datetime_fmt->format_datetime($weekday_peaktime->end) : undef);
push(@weekday_peaktimes,\%wp);
}
return \@weekday_peaktimes;
}
sub resource_from_peaktime_specials {
my ($profile) = @_;
my $datetime_fmt = DateTime::Format::Strptime->new(
pattern => '%F %T',
);
my $rs = $profile->billing_peaktime_specials->search_rs(
undef,
{ order_by => { '-asc' => [ 'start', 'id' ]},
});
my @special_peaktimes = ();
foreach my $special_peaktime ($rs->all) {
my %sp = ();
$sp{start} = ($special_peaktime->start ? $datetime_fmt->format_datetime($special_peaktime->start) : undef);
$sp{stop} = ($special_peaktime->end ? $datetime_fmt->format_datetime($special_peaktime->end) : undef);
push(@special_peaktimes,\%sp);
}
return \@special_peaktimes;
}
1;
=head1 NAME

@ -84,7 +84,7 @@ sub set_fake_time {
M => 60*60*24*30,
y => 60*60*24*365,
);
if (!$o) {
$o = time;
} elsif ($o =~ m/^([+-]\d+)([smhdMy]?)$/) {
@ -130,13 +130,13 @@ sub from_string {
}
sub from_rfc1123_string {
my $s = shift;
my $strp = DateTime::Format::Strptime->new(pattern => RFC_1123_FORMAT_PATTERN,
locale => 'en_US',
on_error => 'undef');
return $strp->parse_datetime($s);
}
@ -175,17 +175,30 @@ sub to_string
}
sub to_rfc1123_string {
my $dt = shift;
my $strp = DateTime::Format::Strptime->new(pattern => RFC_1123_FORMAT_PATTERN,
locale => 'en_US',
on_error => 'undef');
return $strp->format_datetime($dt);
}
sub get_weekday_names {
my $c = shift;
return [
$c->loc('Monday'),
$c->loc('Tuesday'),
$c->loc('Wednesday'),
$c->loc('Thursday'),
$c->loc('Friday'),
$c->loc('Saturday'),
$c->loc('Sunday')
];
}
1;
# vim: set tabstop=4 expandtab:

@ -153,7 +153,7 @@ my @allprofiles = ();
delete $profiles{$c->{_links}->{self}->{href}};
}
}
} while($nexturi);
is(scalar(keys %profiles), 0, "check if all test billing profiles have been found");
@ -178,18 +178,18 @@ my @allprofiles = ();
$req = HTTP::Request->new('GET', $uri.'/'.$firstprofile);
$res = $ua->request($req);
is($res->code, 200, "fetch one contract item");
is($res->code, 200, "fetch one profile item");
my $profile = JSON::from_json($res->decoded_content);
ok(exists $profile->{reseller_id} && $profile->{reseller_id}->is_int, "check existence of reseller_id");
ok(exists $profile->{reseller_id} && $profile->{reseller_id} > 0, "check existence of reseller_id");
ok(exists $profile->{handle}, "check existence of handle");
ok(exists $profile->{name}, "check existence of name");
# PUT same result again
my $old_profile = { %$profile };
delete $profile->{_links};
delete $profile->{_embedded};
$req = HTTP::Request->new('PUT', $uri.'/'.$firstprofile);
# check if it fails without content type
$req->remove_header('Content-Type');
$req->header('Prefer' => "return=minimal");
@ -242,7 +242,7 @@ my @allprofiles = ();
is($mod_profile->{name}, "patched name $t", "check patched replace op");
is($mod_profile->{_links}->{self}->{href}, $firstprofile, "check patched self link");
is($mod_profile->{_links}->{collection}->{href}, '/api/billingprofiles/', "check patched collection link");
$req->content(JSON::to_json(
[ { op => 'replace', path => '/reseller_id', value => undef } ]
@ -261,9 +261,9 @@ my @allprofiles = ();
));
$res = $ua->request($req);
is($res->code, 200, "check patched prepaid");
# TODO: invalid handle etc
$req->content(JSON::to_json(
[ { op => 'replace', path => '/status', value => 'terminated' } ]
));
@ -271,7 +271,126 @@ my @allprofiles = ();
is($res->code, 200, "terminated profile successful");
$req = HTTP::Request->new('GET', $uri.'/'.$firstprofile);
$res = $ua->request($req);
is($res->code, 404, "try to fetch terminated profile");
is($res->code, 404, "try to fetch terminated profile");
}
{
$req = HTTP::Request->new('POST', $uri.'/api/billingprofiles/');
$req->header('Content-Type' => 'application/json');
$req->content(JSON::to_json({
reseller_id => $reseller_id,
handle => "peakweekdays".time,
name => "peak weekdays ".time,
peaktime_weekdays => [
{ weekday => 1,
start => '08:00',
stop => '10:00',
},
{ weekday => 1,
start => '10:01',
stop => '12:00',
},
{ weekday => 2,
start => '10:00',
stop => '12:00',
},
],
}));
$res = $ua->request($req);
is($res->code, 201, "create peaktimes weekday billing profile");
my $profile_uri = $uri.'/'.$res->header('Location');
$req = HTTP::Request->new('GET', $profile_uri);
$res = $ua->request($req);
is($res->code, 200, "fetch POSTed profile");
my $profile = JSON::from_json($res->decoded_content);
my $old_profile = { %$profile };
delete $profile->{_links};
delete $profile->{_embedded};
$req = HTTP::Request->new('PUT', $profile_uri);
$req->header('Content-Type' => 'application/json');
$req->header('Prefer' => 'return=representation');
my $malformed_profile = { %$profile };
$malformed_profile->{peaktime_weekdays} = [
{ weekday => 1,
start => '08:00',
stop => '10:00',
},
{ weekday => 1,
start => '10:00',
stop => '12:00',
},];
$req->content(JSON::to_json($malformed_profile));
$res = $ua->request($req);
is($res->code, 422, "try to update profile using overlapping weekday peaktimes");
my $err = JSON::from_json($res->decoded_content);
ok($err->{message} =~ /overlap/, "check error message in body");
$req->content(JSON::to_json($profile));
$res = $ua->request($req);
is($res->code, 200, "check get2put weekday peaktimes successful");
my $got = JSON::from_json($res->decoded_content);
delete $got->{_links};
delete $got->{_embedded};
is_deeply($got,$profile,"check get2put weekday peaktimes deeply");
}
PEAK:
{
$req = HTTP::Request->new('POST', $uri.'/api/billingprofiles/');
$req->header('Content-Type' => 'application/json');
$req->content(JSON::to_json({
reseller_id => $reseller_id,
handle => "peakspecials".time,
name => "peak specials ".time,
peaktime_special => [
{ start => '2016-01-01 08:00:00',
stop => '2016-01-02 07:59:59',
},
{ start => '2016-01-02 08:00:00',
stop => '2016-01-02 10:00:00',
},
],
}));
$res = $ua->request($req);
is($res->code, 201, "create peaktimes special billing profile");
my $profile_uri = $uri.'/'.$res->header('Location');
$req = HTTP::Request->new('GET', $profile_uri);
$res = $ua->request($req);
is($res->code, 200, "fetch POSTed profile");
my $profile = JSON::from_json($res->decoded_content);
my $old_profile = { %$profile };
delete $profile->{_links};
delete $profile->{_embedded};
$req = HTTP::Request->new('PUT', $profile_uri);
$req->header('Content-Type' => 'application/json');
$req->header('Prefer' => 'return=representation');
my $malformed_profile = { %$profile };
$malformed_profile->{peaktime_special} = [
{ start => '2016-01-01 08:00:00',
stop => '2016-01-02 08:00:00',
},
{ start => '2016-01-02 08:00:00',
stop => '2016-01-02 08:00:01',
},];
$req->content(JSON::to_json($malformed_profile));
$res = $ua->request($req);
is($res->code, 422, "try to update profile using overlapping special peaktimes");
my $err = JSON::from_json($res->decoded_content);
ok($err->{message} =~ /overlap/, "check error message in body");
$req->content(JSON::to_json($profile));
$res = $ua->request($req);
is($res->code, 200, "check get2put special peaktimes successful");
my $got = JSON::from_json($res->decoded_content);
delete $got->{_links};
delete $got->{_embedded};
is_deeply($got,$profile,"check get2put special peaktimes deeply");
}
done_testing;

Loading…
Cancel
Save