From ad3719772ad755a5d042747a00c33bfbdec72495 Mon Sep 17 00:00:00 2001 From: Rene Krenn Date: Sun, 10 Jan 2016 22:23:30 +0100 Subject: [PATCH] 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 --- .../Panel/Controller/API/BillingProfiles.pm | 37 ++- .../Controller/API/BillingProfilesItem.pm | 12 +- lib/NGCP/Panel/Controller/Billing.pm | 10 +- .../Panel/Form/BillingPeaktimeWeekdays.pm | 4 +- .../Panel/Form/BillingProfile/PeaktimeAPI.pm | 65 ++++++ lib/NGCP/Panel/Role/API/BillingProfiles.pm | 49 +++- lib/NGCP/Panel/Role/API/Contracts.pm | 54 ++--- lib/NGCP/Panel/Utils/Billing.pm | 212 +++++++++++++++++- lib/NGCP/Panel/Utils/DateTime.pm | 27 ++- t/api-rest/api-billingprofiles.t | 137 ++++++++++- 10 files changed, 515 insertions(+), 92 deletions(-) create mode 100644 lib/NGCP/Panel/Form/BillingProfile/PeaktimeAPI.pm 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;