diff --git a/lib/NGCP/Panel/Controller/API/CustomerFraudEvents.pm b/lib/NGCP/Panel/Controller/API/CustomerFraudEvents.pm index 1b74bd1074..9660c8e7d5 100644 --- a/lib/NGCP/Panel/Controller/API/CustomerFraudEvents.pm +++ b/lib/NGCP/Panel/Controller/API/CustomerFraudEvents.pm @@ -14,7 +14,7 @@ sub allowed_methods{ } sub api_description { - return 'Defines a list of customers with fraud limits above defined thresholds for a specific interval.'; + return 'Defines a list of current fraud limit violations (the threshold for outgoing call costs per day/month was exceeded) of customers.'; }; sub query_params { @@ -22,18 +22,19 @@ sub query_params { { param => 'reseller_id', description => 'Filter for fraud events belonging to a specific reseller', - query => { - first => sub { - my $q = shift; - return { reseller_id => $q }; - }, - second => sub {}, - }, + }, + { + param => 'contract_id', + description => 'Filter for fraud events of a specific contract', }, { param => 'interval', description => 'Interval filter. values: ["day", "month"].', }, + { + param => 'date', + description => 'Date of the period (YYYY-MM-DD), defaults to current date.', + }, { param => 'notify_status', description => 'Notify status filter. values: ["new", "notified"].', @@ -41,6 +42,19 @@ sub query_params { ]; } +sub order_by_cols { + my ($self, $c) = @_; + my $cols = { + 'contract_id' => 'contract_id', + 'id' => 'id', + 'interval' => 'interval', + 'notified_at' => 'notified_at', + 'notify_status' => 'notify_status', + 'reseller_id' => 'reseller_id', + }; + return $cols; +} + use parent qw/NGCP::Panel::Role::Entities NGCP::Panel::Role::API::CustomerFraudEvents/; sub resource_name{ @@ -60,48 +74,4 @@ __PACKAGE__->set_config({ allowed_roles => [qw/admin reseller/], }); -sub GET :Allow { - my ($self, $c) = @_; - my $page = $c->request->params->{page} // 1; - my $rows = $c->request->params->{rows} // 10; - { - my $items = $self->item_rs($c); - (my $total_count, $items, my $items_rows) = $self->paginate_order_collection($c, $items); - my (@embedded, @links); - my $form = $self->get_form($c); - for my $item (@$items_rows) { - push @embedded, $self->hal_from_item($c, $item, $form); - push @links, Data::HAL::Link->new( - relation => 'ngcp:'.$self->resource_name, - href => sprintf('/%s%d-%s-%s', $c->request->path, $item->id, $item->interval, $item->interval_date), - ); - } - push @links, - Data::HAL::Link->new( - relation => 'curies', - href => 'http://purl.org/sipwise/ngcp-api/#rel-{rel}', - name => 'ngcp', - templated => true, - ), - Data::HAL::Link->new(relation => 'profile', href => 'http://purl.org/sipwise/ngcp-api/'), - $self->collection_nav_links($c, $page, $rows, $total_count, $c->request->path, $c->request->query_params); - - my $hal = Data::HAL->new( - embedded => [@embedded], - links => [@links], - ); - $hal->resource({ - total_count => $total_count, - }); - my $response = HTTP::Response->new(HTTP_OK, undef, - HTTP::Headers->new($hal->http_headers(skip_links => 1)), $hal->as_json); - $c->response->headers($response->headers); - $c->response->body($response->content); - return; - } - return; -} - -1; - -# vim: set tabstop=4 expandtab: +1; \ No newline at end of file diff --git a/lib/NGCP/Panel/Controller/API/CustomerFraudEventsItem.pm b/lib/NGCP/Panel/Controller/API/CustomerFraudEventsItem.pm index ef088a79b5..20bed99122 100644 --- a/lib/NGCP/Panel/Controller/API/CustomerFraudEventsItem.pm +++ b/lib/NGCP/Panel/Controller/API/CustomerFraudEventsItem.pm @@ -27,90 +27,8 @@ sub relation{ return 'http://purl.org/sipwise/ngcp-api/#rel-customerfraudevents'; } -sub query_params { - return [ - { - param => 'interval', - description => 'Interval filter. values: ["day", "month"].', - }, - { - param => 'notify_status', - description => 'Notify status filter. values: ["new", "notified"].', - }, - ]; -} - __PACKAGE__->set_config({ allowed_roles => [qw/admin reseller/], }); -sub GET :Allow { - my ($self, $c, $id) = @_; - { - my $item = $self->item_by_id($c, $id); - last unless $self->resource_exists($c, customerfraudevent => $item); - - my $hal = $self->hal_from_item($c, $item); - - my $response = HTTP::Response->new(HTTP_OK, undef, HTTP::Headers->new( - (map { # XXX Data::HAL must be able to generate links with multiple relations - s|rel="(http://purl.org/sipwise/ngcp-api/#rel-resellers)"|rel="item $1"|r =~ - s/rel=self/rel="item self"/r; - } $hal->http_headers), - ), $hal->as_json); - $c->response->headers($response->headers); - $c->response->body($response->content); - return; - } - return; -} - -sub PATCH :Allow { - my ($self, $c, $id) = @_; - my $schema = $c->model('DB'); - my $guard = $schema->txn_scope_guard; - { - my $preference = $self->require_preference($c); - last unless $preference; - - my $item = $self->item_by_id($c, $id); - my $json = $self->get_valid_patch_data( - c => $c, - id => 1, # TODO: allow non int ids in PATCH - media_type => 'application/json-patch+json', - ops => ["replace"], - ); - last unless $json; - - my $form = $self->get_form($c); - my $old_resource = $self->hal_from_item($c, $item)->resource; - my $resource = $self->apply_patch($c, $old_resource, $json); - last unless $resource; - - $item = $self->update_item($c, $item, undef, $resource, $form); - last unless $item; - - my $hal = $self->hal_from_item($c, $item); - last unless $hal; - - $guard->commit; - - if ('minimal' eq $preference) { - $c->response->status(HTTP_NO_CONTENT); - $c->response->header(Preference_Applied => 'return=minimal'); - $c->response->body(q()); - } else { - my $response = HTTP::Response->new(HTTP_OK, undef, HTTP::Headers->new( - $hal->http_headers, - ), $hal->as_json); - $c->response->headers($response->headers); - $c->response->header(Preference_Applied => 'return=representation'); - $c->response->body($response->content); - } - } - return; -} - 1; - -# vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Form/CustomerFraudEvents/Reseller.pm b/lib/NGCP/Panel/Form/CustomerFraudEvents/Reseller.pm index 1a89505d30..2fed10b58e 100644 --- a/lib/NGCP/Panel/Form/CustomerFraudEvents/Reseller.pm +++ b/lib/NGCP/Panel/Form/CustomerFraudEvents/Reseller.pm @@ -5,41 +5,59 @@ extends 'HTML::FormHandler'; has_field 'id' => ( type => 'PosInteger', - label => 'Customer', - required => 1, + label => 'ID', + required => 0, element_attr => { rel => ['tooltip'], - title => ['Customer that a fraud event belongs to.'] + title => ['ID of the period.'] }, ); -has_field 'interval' => ( - type => 'Text', - label => 'Interval', - required => 1, +has_field 'contract_id' => ( + type => 'PosInteger', + label => 'Contract', + required => 0, element_attr => { rel => ['tooltip'], - title => ['Interval of the fraud events.'] + title => ['Contract ID of the customer/system contract causing the fraud event.'] }, ); -has_field 'interval_date' => ( - type => 'Text', - label => 'Interval Date', +has_field 'interval' => ( + type => 'Select', + label => 'Interval', required => 1, + options => [ + { label => 'current day', value => 'day' }, + { label => 'current month', value => 'month' }, + ], element_attr => { rel => ['tooltip'], - title => ['Interval date of the fraud events.'] + title => ['Period of the fraud event.'] }, ); +#has_field 'interval_date' => ( +# type => 'Text', +# label => 'Interval Date', +# required => 1, +# element_attr => { +# rel => ['tooltip'], +# title => ['Interval date of the fraud events.'] +# }, +#); + has_field 'type' => ( - type => 'Text', + type => 'Select', label => 'Type', required => 1, + options => [ + { label => 'contract fraud preference', value => 'account_limit' }, + { label => 'billing_profile', value => 'profile_limit' }, + ], element_attr => { rel => ['tooltip'], - title => ['Type of the fraud event.'] + title => ['Origin of fraud setting in effect.'] }, ); @@ -65,56 +83,59 @@ has_field 'interval_limit' => ( ); has_field 'interval_lock' => ( - type => 'Integer', + type => '+NGCP::Panel::Field::SubscriberLockSelect', label => 'Interval Lock', required => 1, element_attr => { rel => ['tooltip'], - title => ['Lock type for the interval.'] + title => ['Lock level to apply.'] }, ); -has_field 'interval_notify' => ( - type => 'Text', - label => 'Notify Email', +has_field 'use_reseller_rates' => ( + type => 'Integer', + label => 'Use reseller rates', required => 1, element_attr => { rel => ['tooltip'], - title => ['Email used for this notification.'] + title => ['Whether the reseller rates were used for interval_cost or not.'] }, ); -has_field 'use_reseller_rates' => ( - type => 'Integer', - label => 'Use reseller rates', - required => 1, +has_field 'interval_notify' => ( + type => 'Text', + label => 'Notify Email', + required => 0, element_attr => { rel => ['tooltip'], - title => ['Whether the reseller rates were used for interval_cost or not.'] + title => ['Email used for this notification.'] }, ); has_field 'notify_status' => ( - type => 'Text', + type => 'Select', label => 'Notify Status', required => 1, + options => [ + { label => 'new', value => 'new' }, + { label => 'notified', value => 'notified' }, + ], element_attr => { rel => ['tooltip'], - title => ['Status of the notification.'] + title => ['Status of the notification. \'new\' events are pending to be sent.'] }, ); has_field 'notified_at' => ( type => 'Text', label => 'Notified at', - required => 1, + required => 0, element_attr => { rel => ['tooltip'], - title => ['When the last related notification was sent.'] + title => ['When the last email notification was sent.'] }, ); - 1; =head1 NAME diff --git a/lib/NGCP/Panel/Role/API/CustomerFraudEvents.pm b/lib/NGCP/Panel/Role/API/CustomerFraudEvents.pm index 8224fc6c0c..1d156eb1f3 100644 --- a/lib/NGCP/Panel/Role/API/CustomerFraudEvents.pm +++ b/lib/NGCP/Panel/Role/API/CustomerFraudEvents.pm @@ -8,27 +8,84 @@ use boolean qw(true); use Data::HAL qw(); use Data::HAL::Link qw(); use HTTP::Status qw(:constants); +use NGCP::Panel::Utils::BillingMappings qw(); +use NGCP::Panel::Utils::DateTime qw(); +use DateTime::Format::Strptime qw(); sub _item_rs { - my ($self, $c) = @_; - - my $item_rs = $c->model('DB')->resultset('contract_fraud_events')->search({ - }); + my ($self, $c, $id) = @_; - if (my $interval = $c->request->param('interval')) { - $item_rs = $item_rs->search({ interval => $interval }); + my %cond = (); + if ($c->user->roles eq "admin") { + if (my $reseller_id = $c->request->param('reseller_id')) { + $cond{'contact.reseller_id'} = $reseller_id; + } + } elsif ($c->user->roles eq "reseller") { + $cond{'contact.reseller_id'} = $c->user->reseller_id; + } + if (my $contract_id = $c->request->param('contract_id')) { + $cond{'me.contract_id'} = $contract_id; } if (my $notify_status = $c->request->param('notify_status')) { - $item_rs = $item_rs->search({ notify_status => $notify_status }); + $cond{'notify_status'} = $notify_status; } - - if($c->user->roles eq "admin") { - # - } elsif($c->user->roles eq "reseller") { - $item_rs = $item_rs->search({ reseller_id => $c->user->reseller_id }); + if ($id) { + $cond{'me.id'} = $id; + } + my $attr = { + join => { 'contract' => 'contact' }, + }; + + my $dtf = $c->model('DB')->storage->datetime_parser; + my $now; + my $datetime_fmt = DateTime::Format::Strptime->new( + pattern => '%Y-%m-%d', + ); + unless ($now = $datetime_fmt->parse_datetime($c->request->param('date'))) { + $now = NGCP::Panel::Utils::DateTime::current_local(); + } + my $first_of_month = $now->clone; + $first_of_month->set_day(1); + my $day_rs = $c->model('DB')->resultset('cdr_period_costs')->search_rs({ + 'period' => 'day', + 'period_date' => $dtf->format_date($now), + 'direction' => 'out', + 'fraud_limit_exceeded' => 1, + 'contract.status' => 'active', + %cond, + },$attr); + $now->subtract(days => 1); + my $previous_day_rs = $c->model('DB')->resultset('cdr_period_costs')->search_rs({ + 'period' => 'day', + 'period_date' => $dtf->format_date($now), + 'direction' => 'out', + 'fraud_limit_exceeded' => 1, + 'contract.status' => 'active', + %cond, + },$attr); + my $month_rs = $c->model('DB')->resultset('cdr_period_costs')->search_rs({ + 'period' => 'month', + 'period_date' => $dtf->format_date($first_of_month), + 'direction' => 'out', + 'fraud_limit_exceeded' => 1, + 'contract.status' => 'active', + %cond, + },$attr); + + my $interval = $c->request->param('interval'); + my $rs; + if (defined $interval and $interval eq 'day') { + $rs = $day_rs->union_all($previous_day_rs); + } elsif (defined $interval and $interval eq 'month') { + $rs = $month_rs; + } elsif (not defined $interval) { + $rs = $day_rs->union_all([$previous_day_rs, $month_rs]); + } else { + $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Invalid interval '$interval'"); + return; } - return $item_rs; + return $rs; } sub get_form { @@ -40,49 +97,91 @@ sub get_form { } } -sub hal_from_item { - my ($self, $c, $item, $form) = @_; - - my %resource = $item->get_inflated_columns; - - my $hal = Data::HAL->new( - links => [ - Data::HAL::Link->new( - relation => 'curies', - href => 'http://purl.org/sipwise/ngcp-api/#rel-{rel}', - name => 'ngcp', - templated => true, - ), - Data::HAL::Link->new(relation => 'collection', href => sprintf('/api/%s/', $self->resource_name)), - Data::HAL::Link->new(relation => 'profile', href => 'http://purl.org/sipwise/ngcp-api/'), - Data::HAL::Link->new(relation => 'self', href => sprintf("%s%d", $self->dispatch_path, $item->id)), - ], - relation => 'ngcp:'.$self->resource_name, - ); +sub resource_from_item { + my ($self, $c, $item) = @_; #$form - $form //= $self->get_form($c); - return unless $self->validate_form( - c => $c, - form => $form, - resource => \%resource, - run => 0, + my %cpc = $item->get_inflated_columns; + my %resource; + my $billing_mapping = NGCP::Panel::Utils::BillingMappings::get_actual_billing_mapping(c => $c, + now => NGCP::Panel::Utils::DateTime::epoch_local($item->last_cdr_start_time), + contract => $item->contract, ); + my $billing_profile = $billing_mapping->billing_profile; + my $contract_fraud_preference = $item->contract->contract_fraud_preference; + $resource{'contract_id'} = $item->contract->id; + $resource{'reseller_id'} = $item->contract->contact->reseller_id; + $resource{'interval'} = $cpc{'period'}; + $resource{'type'} = $cpc{'fraud_limit_type'}; + $resource{'type'} = 'account_limit' if $resource{'type'} eq 'contract'; + $resource{'type'} = 'profile_limit' if $resource{'type'} eq 'billing_profile'; + + if ($billing_profile->fraud_use_reseller_rates) { + $resource{'interval_cost'} = $cpc{'customer_cost'}; + } else { + $resource{'interval_cost'} = $cpc{'reseller_cost'}; + } + if ('month' eq $cpc{'period'}) { + if ('contract' eq $cpc{'fraud_limit_type'}) { + if ($contract_fraud_preference) { + $resource{'interval_limit'} = $contract_fraud_preference->fraud_interval_limit; + $resource{'interval_lock'} = $contract_fraud_preference->fraud_interval_lock; + $resource{'interval_notify'} = $contract_fraud_preference->fraud_interval_notify; + } else { + $self->debug($c, "no contract fraud preference any more for contract ID $cpc{contract_id}"); + $resource{'interval_limit'} = undef; + $resource{'interval_lock'} = undef; + $resource{'interval_notify'} = undef; + } + } elsif ('billing_profile' eq $cpc{'fraud_limit_type'}) { + $resource{'interval_limit'} = $billing_profile->fraud_interval_limit; + $resource{'interval_lock'} = $billing_profile->fraud_interval_lock; + $resource{'interval_notify'} = $billing_profile->fraud_interval_notify; + } else { + $self->debug($c, "unsupported fraud limit type '$cpc{fraud_limit_type}'"); + $resource{'interval_limit'} = undef; + $resource{'interval_lock'} = undef; + $resource{'interval_notify'} = undef; + } + } elsif ('day' eq $cpc{'period'}) { + if ('contract' eq $cpc{'fraud_limit_type'}) { + if ($contract_fraud_preference) { + $resource{'interval_limit'} = $contract_fraud_preference->fraud_daily_limit; + $resource{'interval_lock'} = $contract_fraud_preference->fraud_daily_lock; + $resource{'interval_notify'} = $contract_fraud_preference->fraud_daily_notify; + } else { + + } + } elsif ('billing_profile' eq $cpc{'fraud_limit_type'}) { + $resource{'interval_limit'} = $billing_profile->fraud_daily_limit; + $resource{'interval_lock'} = $billing_profile->fraud_daily_lock; + $resource{'interval_notify'} = $billing_profile->fraud_daily_notify; + } else { + $self->debug($c, "unsupported fraud limit type '$cpc{fraud_limit_type}'"); + $resource{'interval_limit'} = undef; + $resource{'interval_lock'} = undef; + $resource{'interval_notify'} = undef; + } + } else { + $self->debug($c, "unsupported fraud interval '$cpc{'period'}'"); + $resource{'interval_limit'} = undef; + $resource{'interval_lock'} = undef; + $resource{'interval_notify'} = undef; + } + $resource{'use_reseller_rates'} = $billing_profile->fraud_use_reseller_rates; + $resource{'notify_status'} = $cpc{'notify_status'}; + my $datetime_fmt = DateTime::Format::Strptime->new( + pattern => '%F %T', + ); + $resource{'notified_at'} = undef; + $resource{'notified_at'} = $datetime_fmt->format_datetime($cpc{notified_at}) if defined $cpc{notified_at}; + + return \%resource; - $resource{id} = int($item->id); - $hal->resource({%resource}); - return $hal; } sub item_by_id { my ($self, $c, $id) = @_; - my $item_rs = $self->item_rs($c); - my ($contract_id, $period, $period_date) = split(/-/, $id, 3); - - return $item_rs->search_rs({ - id => $contract_id, - interval => $period, - interval_date => $period_date - })->first; + return $self->item_rs($c,$id)->first; } sub update_item { @@ -95,24 +194,18 @@ sub update_item { resource => $resource, ); - my $cpc_rs = $c->model('DB')->resultset('cdr_period_costs')->search({ - contract_id => $item->id, - period => $item->interval, - period_date => $item->interval_date - }); - my $cpc = $cpc_rs->first; - unless ($cpc) { - $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Customer fraud event does not exist"); + try { + # only update r/w fields + $item->update({ + map { $_ => $resource->{$_} } qw(notify_status notified_at) + })->discard_changes; + } catch($e) { + $c->log->error("failed to update customer fraud event: $e"); + $self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Failed to update customer fraud event."); return; - } - - # only update r/w fields - $cpc->update({ - map { $_ => $resource->{$_} } qw(notify_status notified_at) - }); + }; return $item; } 1; -# vim: set tabstop=4 expandtab: