diff --git a/lib/NGCP/Panel/Controller/API/BillingProfiles.pm b/lib/NGCP/Panel/Controller/API/BillingProfiles.pm
index aae0f340ad..7f5d94af63 100644
--- a/lib/NGCP/Panel/Controller/API/BillingProfiles.pm
+++ b/lib/NGCP/Panel/Controller/API/BillingProfiles.pm
@@ -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 Billing Fees and Billing Zones and can be assigned to Customers and System Contracts.'
);
@@ -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) = @_;
diff --git a/lib/NGCP/Panel/Controller/API/BillingProfilesItem.pm b/lib/NGCP/Panel/Controller/API/BillingProfilesItem.pm
index f1332b1140..36be2cd27d 100644
--- a/lib/NGCP/Panel/Controller/API/BillingProfilesItem.pm
+++ b/lib/NGCP/Panel/Controller/API/BillingProfilesItem.pm
@@ -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(@_);
diff --git a/lib/NGCP/Panel/Controller/Billing.pm b/lib/NGCP/Panel/Controller/Billing.pm
index 3d4632d52c..c1acffa10f 100644
--- a/lib/NGCP/Panel/Controller/Billing.pm
+++ b/lib/NGCP/Panel/Controller/Billing.pm
@@ -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) {
diff --git a/lib/NGCP/Panel/Form/BillingPeaktimeWeekdays.pm b/lib/NGCP/Panel/Form/BillingPeaktimeWeekdays.pm
index 67d5143516..669c5e8820 100644
--- a/lib/NGCP/Panel/Form/BillingPeaktimeWeekdays.pm
+++ b/lib/NGCP/Panel/Form/BillingPeaktimeWeekdays.pm
@@ -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);
}
diff --git a/lib/NGCP/Panel/Form/BillingProfile/PeaktimeAPI.pm b/lib/NGCP/Panel/Form/BillingProfile/PeaktimeAPI.pm
new file mode 100644
index 0000000000..39affd3c3e
--- /dev/null
+++ b/lib/NGCP/Panel/Form/BillingProfile/PeaktimeAPI.pm
@@ -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;
diff --git a/lib/NGCP/Panel/Role/API/BillingProfiles.pm b/lib/NGCP/Panel/Role/API/BillingProfiles.pm
index f84fe6c038..bc3bcd5360 100644
--- a/lib/NGCP/Panel/Role/API/BillingProfiles.pm
+++ b/lib/NGCP/Panel/Role/API/BillingProfiles.pm
@@ -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;
}
diff --git a/lib/NGCP/Panel/Role/API/Contracts.pm b/lib/NGCP/Panel/Role/API/Contracts.pm
index bdeeb90c7e..2e501828aa 100644
--- a/lib/NGCP/Panel/Role/API/Contracts.pm
+++ b/lib/NGCP/Panel/Role/API/Contracts.pm
@@ -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;
}
diff --git a/lib/NGCP/Panel/Utils/Billing.pm b/lib/NGCP/Panel/Utils/Billing.pm
index d1b1ceae90..aec865f408 100644
--- a/lib/NGCP/Panel/Utils/Billing.pm
+++ b/lib/NGCP/Panel/Utils/Billing.pm
@@ -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
diff --git a/lib/NGCP/Panel/Utils/DateTime.pm b/lib/NGCP/Panel/Utils/DateTime.pm
index 5b5c04074b..9efa5ec30a 100644
--- a/lib/NGCP/Panel/Utils/DateTime.pm
+++ b/lib/NGCP/Panel/Utils/DateTime.pm
@@ -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:
diff --git a/t/api-rest/api-billingprofiles.t b/t/api-rest/api-billingprofiles.t
index 0301cd6d83..28cb5329f0 100644
--- a/t/api-rest/api-billingprofiles.t
+++ b/t/api-rest/api-billingprofiles.t
@@ -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;