From 2b7a1a33a8026d76d714daf2f72dda931bf765c0 Mon Sep 17 00:00:00 2001 From: Rene Krenn Date: Mon, 27 Aug 2018 10:15:44 +0200 Subject: [PATCH] TT#41553 TT#41554 billing fee match_mode Change-Id: I77e2cccdcb320e55642f3166bc49ca4f491a8eea --- lib/NGCP/Panel/Controller/API/BillingFees.pm | 2 +- lib/NGCP/Panel/Controller/Billing.pm | 13 +++- lib/NGCP/Panel/Controller/Voucher.pm | 22 +++--- lib/NGCP/Panel/Form/BillingFee.pm | 70 ++++++++++++++------ lib/NGCP/Panel/Form/BillingFee/API.pm | 2 +- lib/NGCP/Panel/Role/API/BillingFees.pm | 6 +- lib/NGCP/Panel/Utils/Billing.pm | 48 ++++++++++++-- ngcp_panel.conf | 1 + 8 files changed, 121 insertions(+), 43 deletions(-) diff --git a/lib/NGCP/Panel/Controller/API/BillingFees.pm b/lib/NGCP/Panel/Controller/API/BillingFees.pm index 4c981c5a2e..4bb5842262 100644 --- a/lib/NGCP/Panel/Controller/API/BillingFees.pm +++ b/lib/NGCP/Panel/Controller/API/BillingFees.pm @@ -189,7 +189,7 @@ sub POST :Allow { $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Invalid 'billing_zone_id'."); last; } - + $resource->{match_mode} = 'regex_longest_pattern' unless $resource->{match_mode}; last unless $self->validate_form( c => $c, resource => $resource, diff --git a/lib/NGCP/Panel/Controller/Billing.pm b/lib/NGCP/Panel/Controller/Billing.pm index 7774769664..7ece18fe70 100644 --- a/lib/NGCP/Panel/Controller/Billing.pm +++ b/lib/NGCP/Panel/Controller/Billing.pm @@ -333,11 +333,20 @@ sub terminate :Chained('base') :PathPart('terminate') :Args(0) { sub fees_list :Chained('base') :PathPart('fees') :CaptureArgs(0) { my ($self, $c) = @_; + $c->stash->{fee_dt_columns} = NGCP::Panel::Utils::Datatables::set_columns($c, [ { name => 'id', search => 1, title => $c->loc('#') }, { name => 'source', search => 1, title => $c->loc('Source Pattern') }, { name => 'destination', search => 1, title => $c->loc('Destination Pattern') }, - { name => 'direction', search => 1, title => $c->loc('Match Direction') }, + { name => 'match_mode', search => 0, title => $c->loc('Match Mode'), + custom_renderer => 'function ( data, type, full ) {'. + 'if(full.match_mode == "regex_longest_pattern"){return "' . $c->loc('Regular expression - longest pattern') . '";}'. + 'else if(full.match_mode == "regex_longest_match"){return "' . $c->loc('Regular expression - longest match') . '";}'. + 'else if(full.match_mode == "prefix"){return "' . $c->loc('Prefix string') . '";}'. + 'else if(full.match_mode == "exact_destination"){return "' . $c->loc('Exact string (destination)') . '";}'. + '}', + }, + { name => 'direction', search => 1, title => $c->loc('Direction') }, { name => 'billing_zone.detail', search => 1, title => $c->loc('Billing Zone') }, ]); $c->stash(template => 'billing/fees.tt'); @@ -410,6 +419,7 @@ sub fees_create :Chained('fees_list') :PathPart('create') :Args(0) { ); if($form->validated) { $form->values->{source} ||= '.'; + $form->values->{match_mode} ||= 'regex_longest_pattern'; my $schema = $c->model('DB'); $schema->txn_do(sub { NGCP::Panel::Utils::Billing::insert_unique_billing_fees( @@ -529,6 +539,7 @@ sub fees_edit :Chained('fees_base') :PathPart('edit') :Args(0) { ); if($posted && $form->validated) { $form->values->{source} ||= '.'; + $form->values->{match_mode} ||= 'regex_longest_pattern'; $form->values->{billing_zone_id} = $form->values->{billing_zone}{id}; delete $form->values->{billing_zone}; $c->stash->{'fee_result'} diff --git a/lib/NGCP/Panel/Controller/Voucher.pm b/lib/NGCP/Panel/Controller/Voucher.pm index 93a0319dbc..1fe57eb243 100644 --- a/lib/NGCP/Panel/Controller/Voucher.pm +++ b/lib/NGCP/Panel/Controller/Voucher.pm @@ -28,7 +28,7 @@ sub auto :Does(ACL) :ACLDetachTo('/denied_page') :AllowedRole(admin) :AllowedRol sub voucher_list :Chained('/') :PathPart('voucher') :CaptureArgs(0) { my ( $self, $c ) = @_; - + my $voucher_rs = $c->model('DB')->resultset('vouchers'); #->search_rs(undef, { #'join' => { 'customer' => 'contact'}, #'+select' => [ @@ -73,25 +73,25 @@ sub _process_dt_voucher_rows :Private { sub ajax :Chained('voucher_list') :PathPart('ajax') :Args(0) { my ($self, $c) = @_; - + my $resultset = $c->stash->{voucher_rs}; NGCP::Panel::Utils::Datatables::process($c, $resultset, $c->stash->{voucher_dt_columns}, sub { my $row = shift; return _process_dt_voucher_rows($c,$row); }); - + $c->detach( $c->view("JSON") ); } sub ajax_package_filter :Chained('voucher_list') :PathPart('ajax/package') :Args(1) { my ($self, $c, $package_id) = @_; - + my $resultset = $c->stash->{voucher_rs}->search_rs({ package_id => $package_id },undef); NGCP::Panel::Utils::Datatables::process($c, $resultset, $c->stash->{voucher_dt_columns}, sub { my $row = shift; return _process_dt_voucher_rows($c,$row); }); - + $c->detach( $c->view("JSON") ); } @@ -188,7 +188,7 @@ sub edit :Chained('base') :PathPart('edit') { if($posted && $form->validated) { try { if($c->user->is_superuser) { - $form->values->{reseller_id} = $form->values->{reseller}{id}; + $form->values->{reseller_id} = $form->values->{reseller}{id}; } else { $form->values->{reseller_id} = $c->user->reseller_id; } @@ -267,7 +267,7 @@ sub create :Chained('voucher_list') :PathPart('create') :Args(0) { if($posted && $form->validated) { try { if($c->user->is_superuser) { - $form->values->{reseller_id} = $form->values->{reseller}{id}; + $form->values->{reseller_id} = $form->values->{reseller}{id}; } else { $form->values->{reseller_id} = $c->user->reseller_id; } @@ -275,7 +275,7 @@ sub create :Chained('voucher_list') :PathPart('create') :Args(0) { $form->values->{customer_id} = $form->values->{customer}{id}; delete $form->values->{customer}; $form->values->{package_id} = $form->values->{package}{id}; - delete $form->values->{package}; + delete $form->values->{package}; $form->values->{created_at} = NGCP::Panel::Utils::DateTime::current_local; if($form->values->{valid_until} =~ /^\d{4}\-\d{2}\-\d{2}$/) { $form->values->{valid_until} = NGCP::Panel::Utils::DateTime::from_string($form->values->{valid_until}) @@ -286,7 +286,7 @@ sub create :Chained('voucher_list') :PathPart('create') :Args(0) { $c->session->{created_objects}->{voucher} = { id => $voucher->id }; delete $c->session->{created_objects}->{reseller}; delete $c->session->{created_objects}->{customer}; - delete $c->session->{created_objects}->{profile_package}; + delete $c->session->{created_objects}->{profile_package}; NGCP::Panel::Utils::Message::info( c => $c, desc => $c->loc('Billing voucher successfully created'), @@ -310,12 +310,12 @@ sub voucher_upload :Chained('voucher_list') :PathPart('upload') :Args(0) { $c->detach('/denied_page') unless($c->user->billing_data); - + my $form = NGCP::Panel::Form::get("NGCP::Panel::Form::Voucher::Upload", $c); my $upload = $c->req->upload('upload_vouchers'); my $posted = $c->req->method eq 'POST'; my @params = ( - upload_fees => $posted ? $upload : undef, + upload_vouchers => $posted ? $upload : undef, ); $form->process( posted => $posted, diff --git a/lib/NGCP/Panel/Form/BillingFee.pm b/lib/NGCP/Panel/Form/BillingFee.pm index 16229fdbc9..8e2a440b60 100644 --- a/lib/NGCP/Panel/Form/BillingFee.pm +++ b/lib/NGCP/Panel/Form/BillingFee.pm @@ -5,28 +5,45 @@ extends 'HTML::FormHandler'; use HTML::FormHandler::Widget::Block::Bootstrap; use NGCP::Panel::Field::BillingZone; +use NGCP::Panel::Utils::Billing qw(); has '+widget_wrapper' => ( default => 'Bootstrap' ); has_field 'submitid' => ( type => 'Hidden' ); sub build_render_list {[qw/submitid fields actions/]} sub build_form_element_class { [qw/form-horizontal/] } +has_field 'match_mode' => ( + type => 'Select', + options => [ + { value => 'regex_longest_pattern', label => 'Regular expression - longest pattern' }, + { value => 'regex_longest_match', label => 'Regular expression - longest match' }, + { value => 'prefix', label => 'Prefix string' }, + { value => 'exact_destination', label => 'Exact string (destination)' }, + ], + default => 'regex_longest_pattern', + required => 1, + element_attr => { + rel => ['tooltip'], + title => ['The mode how the the fee\'s source/destination has to match a call\'s source/destination.'] + }, +); + has_field 'source' => ( - type => '+NGCP::Panel::Field::Regexp', + type => 'Text', maxlength => 255, element_attr => { rel => ['tooltip'], - title => ['A PCRE regular expression to match the calling number (e.g. ^.+$).'] + title => ['A string (eg. 431001), string prefix (eg. 43) or PCRE regular expression (eg. ^.+$) to match the calling number or sip uri.'] }, ); has_field 'destination' => ( - type => '+NGCP::Panel::Field::Regexp', + type => 'Text', maxlength => 255, required => 1, element_attr => { rel => ['tooltip'], - title => ['A PCRE regular expression to match the called number (e.g. ^431.+$).'] + title => ['A string (eg. 431001), string prefix (eg. 43) or PCRE regular expression (eg. ^.+$) to match the called number or sip uri.'] }, ); @@ -60,7 +77,7 @@ has_field 'onpeak_init_rate' => ( precision => 14, element_attr => { rel => ['tooltip'], - title => ['The cost of the first interval in cents per second (e.g. 0.90).'] + title => ['The cost per second of the first interval during onpeak hours (e.g. 0.90 cent).'] }, default => 0, ); @@ -69,11 +86,10 @@ has_field 'onpeak_init_interval' => ( type => 'Integer', element_attr => { rel => ['tooltip'], - title => ['The length of the first interval in seconds (e.g. 60).'] + title => ['The length of the first interval during onpeak hours in seconds (e.g. 60).'] }, default => 60, required => 1, - validate_method => \&validate_interval, ); has_field 'onpeak_follow_rate' => ( @@ -82,7 +98,7 @@ has_field 'onpeak_follow_rate' => ( precision => 14, element_attr => { rel => ['tooltip'], - title => ['The cost of each following interval in cents per second (e.g. 0.90).'] + title => ['The cost per second of each following interval during onpeak hours in cents (e.g. 0.90 cents).'] }, default => 0, ); @@ -91,11 +107,10 @@ has_field 'onpeak_follow_interval' => ( type => 'Integer', element_attr => { rel => ['tooltip'], - title => ['The length of each following interval in seconds (e.g. 30).'] + title => ['The length of each following interval during onpeak hours in seconds (e.g. 30).'] }, default => 60, required => 1, - validate_method => \&validate_interval, ); has_field 'offpeak_init_rate' => ( @@ -104,7 +119,7 @@ has_field 'offpeak_init_rate' => ( precision => 14, element_attr => { rel => ['tooltip'], - title => ['The cost of the first interval in cents per second (e.g. 0.90).'] + title => ['The cost per second of the first interval during offpeak hours in cents (e.g. 0.70 cents).'] }, default => 0, ); @@ -113,11 +128,10 @@ has_field 'offpeak_init_interval' => ( type => 'Integer', element_attr => { rel => ['tooltip'], - title => ['The length of the first interval in seconds (e.g. 60).'] + title => ['The length of the first interval during offpeak hours in seconds (e.g. 60).'] }, default => 60, required => 1, - validate_method => \&validate_interval, ); has_field 'offpeak_follow_rate' => ( @@ -126,7 +140,7 @@ has_field 'offpeak_follow_rate' => ( precision => 14, element_attr => { rel => ['tooltip'], - title => ['The cost of each following interval in cents per second (e.g. 0.90).'] + title => ['The cost per second of each following interval during offpeak hours in cents (e.g. 0.70 cents).'] }, default => 0, ); @@ -135,11 +149,10 @@ has_field 'offpeak_follow_interval' => ( type => 'Integer', element_attr => { rel => ['tooltip'], - title => ['The length of each following interval in seconds (e.g. 30).'] + title => ['The length of each following interval during offpeak hours in seconds (e.g. 30).'] }, default => 60, required => 1, - validate_method => \&validate_interval, ); has_field 'use_free_time' => ( @@ -161,7 +174,7 @@ has_field 'save' => ( has_block 'fields' => ( tag => 'div', class => [qw/modal-body/], - render_list => [qw/billing_zone source destination direction + render_list => [qw/billing_zone match_mode source destination direction onpeak_init_rate onpeak_init_interval onpeak_follow_rate onpeak_follow_interval offpeak_init_rate offpeak_init_interval offpeak_follow_rate offpeak_follow_interval use_free_time @@ -174,12 +187,25 @@ has_block 'actions' => ( render_list => [qw/save/], ); -sub validate_interval { - my ($self, $field) = @_; +sub validate { + + my ($self) = @_; + my $c = $self->ctx; + return unless $c; + + NGCP::Panel::Utils::Billing::validate_billing_fee( + $self->values, + sub { + my ($field,$error,$error_detail) = @_; + $self->field($field)->add_error($self->field($field)->label . ' ' . $error); + return 1; + }, + sub { + my ($field) = @_; + return $self->field($field)->value; + }, + ) - if(int($field->value) < 1) { - $field->add_error("Invalid interval, must be bigger than 0"); - } } 1; diff --git a/lib/NGCP/Panel/Form/BillingFee/API.pm b/lib/NGCP/Panel/Form/BillingFee/API.pm index 55914f226c..ebbc7cc0fd 100644 --- a/lib/NGCP/Panel/Form/BillingFee/API.pm +++ b/lib/NGCP/Panel/Form/BillingFee/API.pm @@ -25,7 +25,7 @@ has_field 'purge_existing' => ( has_block 'fields' => ( tag => 'div', class => [qw/modal-body/], - render_list => [qw/purge_existing billing_zone billing_profile_id source destination direction + render_list => [qw/purge_existing billing_zone billing_profile_id match_mode source destination direction onpeak_init_rate onpeak_init_interval onpeak_follow_rate onpeak_follow_interval offpeak_init_rate offpeak_init_interval offpeak_follow_rate offpeak_follow_interval use_free_time diff --git a/lib/NGCP/Panel/Role/API/BillingFees.pm b/lib/NGCP/Panel/Role/API/BillingFees.pm index 12cc0a3902..c957fb5c03 100644 --- a/lib/NGCP/Panel/Role/API/BillingFees.pm +++ b/lib/NGCP/Panel/Role/API/BillingFees.pm @@ -96,7 +96,7 @@ sub update_fee { } $form //= $self->get_form($c); - + $resource->{match_mode} = 'regex_longest_pattern' unless $resource->{match_mode}; return unless $self->validate_form( c => $c, form => $form, @@ -136,7 +136,7 @@ sub get_billing_zone{ my($self,$c,$profile,$resource) = @_; if( (!defined $profile) && defined $resource->{billing_profile_id} ){ - $profile = $c->model('DB')->resultset('billing_profiles')->find($resource->{billing_profile_id}); + $profile = $c->model('DB')->resultset('billing_profiles')->find($resource->{billing_profile_id}); } if(!defined $profile){ $c->log->debug("in get_billing_zone: no profile;"); @@ -144,7 +144,7 @@ sub get_billing_zone{ } my $zone; - + # in case of implicit zone declaration (name/detail instead of id), # find or create the zone if( (!defined $resource->{billing_zone_id}) && diff --git a/lib/NGCP/Panel/Utils/Billing.pm b/lib/NGCP/Panel/Utils/Billing.pm index 8fe93be8f7..6aabefccdf 100644 --- a/lib/NGCP/Panel/Utils/Billing.pm +++ b/lib/NGCP/Panel/Utils/Billing.pm @@ -188,6 +188,39 @@ sub prepare_peaktime_specials { return 1; } +sub validate_billing_fee { + my ($values,$error_code,$value_code) = @_; + $value_code //= sub { + my $field = shift; + return $values->{$field}; + }; + + my $match_mode = $values->{match_mode}; + if (defined $match_mode + and ('regex_longest_pattern' eq $match_mode + or 'regex_longest_match' eq $match_mode)) { + foreach my $field (qw(source destination)) { + my $pattern = &$value_code($field); + if (defined $pattern and length($pattern) > 0) { + eval { + qr/$pattern/; + }; + if ($@) { + return 0 unless &$error_code($field,'no valid regexp',$@); + } + } + } + } + + foreach my $field (qw(onpeak_init_interval onpeak_follow_interval offpeak_init_interval offpeak_follow_interval)) { + if(int(&$value_code($field)) < 1) { + return 0 unless &$error_code($field,'must be greater than 0'); + } + } + + return 1; +} + sub process_billing_fees{ my(%params) = @_; my ($c,$data,$profile,$schema) = @params{qw/c data profile schema/}; @@ -211,10 +244,6 @@ sub process_billing_fees{ next; } @fields = $csv->fields(); - unless (scalar @fields == scalar @cols) { - push @fails, $linenum; - next; - } my $row = {}; @{$row}{@cols} = @fields; unless($row->{zone}){ @@ -232,6 +261,17 @@ sub process_billing_fees{ $row->{billing_zone_id} = $zones{$k}; delete $row->{zone}; delete $row->{zone_detail}; + $row->{match_mode} = 'regex_longest_pattern' unless $row->{match_mode}; + unless (validate_billing_fee($row, + sub { + my ($field,$error,$error_detail) = @_; + return 0; + }, + undef, + )) { + push @fails, $linenum; + next; + } push @fees, $row; } diff --git a/ngcp_panel.conf b/ngcp_panel.conf index 18b1d38227..664ea542dc 100644 --- a/ngcp_panel.conf +++ b/ngcp_panel.conf @@ -122,6 +122,7 @@ log4perl.appender.Default.layout.ConversionPattern=%d{ISO8601} [%p] [%F +%L] %m{ element_order offpeak_follow_rate element_order offpeak_follow_interval element_order use_free_time + element_order match_mode