From bdb129ec67f41c5e628212d92b75e96d7f839ee3 Mon Sep 17 00:00:00 2001 From: Andreas Granig Date: Sat, 7 Dec 2013 13:27:50 +0100 Subject: [PATCH] MT#5299 API: Add billing fee handling. --- lib/NGCP/Panel/Controller/API/BillingFees.pm | 214 ++++++++++++++++++ .../Panel/Controller/API/BillingFeesItem.pm | 192 ++++++++++++++++ .../Panel/Controller/API/BillingProfiles.pm | 5 +- lib/NGCP/Panel/Form/BillingFee.pm | 24 +- lib/NGCP/Panel/Role/API/BillingFees.pm | 122 ++++++++++ 5 files changed, 552 insertions(+), 5 deletions(-) create mode 100644 lib/NGCP/Panel/Controller/API/BillingFees.pm create mode 100644 lib/NGCP/Panel/Controller/API/BillingFeesItem.pm create mode 100644 lib/NGCP/Panel/Role/API/BillingFees.pm diff --git a/lib/NGCP/Panel/Controller/API/BillingFees.pm b/lib/NGCP/Panel/Controller/API/BillingFees.pm new file mode 100644 index 0000000000..5212f25d5c --- /dev/null +++ b/lib/NGCP/Panel/Controller/API/BillingFees.pm @@ -0,0 +1,214 @@ +package NGCP::Panel::Controller::API::BillingFees; +use Sipwise::Base; +use namespace::sweep; +use boolean qw(true); +use Data::HAL qw(); +use Data::HAL::Link qw(); +use HTTP::Headers qw(); +use HTTP::Status qw(:constants); +use MooseX::ClassAttribute qw(class_has); +use NGCP::Panel::Utils::DateTime; +use NGCP::Panel::Form::BillingFee qw(); +use Path::Tiny qw(path); +BEGIN { extends 'Catalyst::Controller::ActionRole'; } +require Catalyst::ActionRole::ACL; +require Catalyst::ActionRole::CheckTrailingSlash; +require Catalyst::ActionRole::HTTPMethods; +require Catalyst::ActionRole::RequireSSL; + +with 'NGCP::Panel::Role::API'; +with 'NGCP::Panel::Role::API::BillingFees'; + +class_has('resource_name', is => 'ro', default => 'billingfees'); +class_has('dispatch_path', is => 'ro', default => '/api/billingfees/'); +class_has('relation', is => 'ro', default => 'http://purl.org/sipwise/ngcp-api/#rel-billingfees'); + +__PACKAGE__->config( + action => { + map { $_ => { + ACLDetachTo => '/api/root/invalid_user', + AllowedRole => 'api_admin', + Args => 0, + Does => [qw(ACL CheckTrailingSlash RequireSSL)], + Method => $_, + Path => __PACKAGE__->dispatch_path, + } } @{ __PACKAGE__->allowed_methods } + }, + action_roles => [qw(HTTPMethods)], +); + +sub auto :Private { + my ($self, $c) = @_; + + $self->set_body($c); + $self->log_request($c); +} + +sub GET :Allow { + my ($self, $c) = @_; + my $page = $c->request->params->{page} // 1; + my $rows = $c->request->params->{rows} // 10; + { + my $fees = $c->model('DB')->resultset('billing_fees'); + if($c->request->query_parameters->{billing_profile_id}) { + $fees = $fees->search({ + billing_profile_id => $c->request->query_parameters->{billing_profile_id}, + }); + }; + + if($c->user->roles eq "api_admin") { + } elsif($c->user->roles eq "api_reseller") { + $fees = $fees->search({ + 'billing_profile.reseller_id' => $c->user->reseller_id + }, { + join => 'billing_profile', + }); + } else { + $fees = $fees->search({ + 'billing_profile.reseller_id' => $c->user->contract->contact->reseller_id, + }, { + join => 'billing_profile', + }); + } + my $total_count = int($fees->count); + $fees = $fees->search(undef, { + page => $page, + rows => $rows, + }); + my (@embedded, @links); + my $form = NGCP::Panel::Form::BillingFee->new; + for my $fee ($fees->all) { + push @embedded, $self->hal_from_fee($c, $fee, $form); + push @links, Data::HAL::Link->new( + relation => 'ngcp:'.$self->resource_name, + href => sprintf('/%s%d', $c->request->path, $fee->id), + ); + } + 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/'), + Data::HAL::Link->new(relation => 'self', href => sprintf('/%s?page=%s&rows=%s', $c->request->path, $page, $rows)); + + if(($total_count / $rows) > $page ) { + push @links, Data::HAL::Link->new(relation => 'next', href => sprintf('/%s?page=%d&rows=%d', $c->request->path, $page + 1, $rows)); + } + if($page > 1) { + push @links, Data::HAL::Link->new(relation => 'prev', href => sprintf('/%s?page=%d&rows=%d', $c->request->path, $page - 1, $rows)); + } + + my $hal = Data::HAL->new( + embedded => [@embedded], + links => [@links], + ); + $hal->resource({ + total_count => $total_count, + }); + my $rname = $self->resource_name; + 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-$rname)"|rel="item $1"|; + s/rel=self/rel="collection self"/; + $_ + } $hal->http_headers), + ), $hal->as_json); + $c->response->headers($response->headers); + $c->response->body($response->content); + return; + } + return; +} + +sub HEAD :Allow { + my ($self, $c) = @_; + $c->forward(qw(GET)); + $c->response->body(q()); + return; +} + +sub OPTIONS :Allow { + my ($self, $c) = @_; + my $allowed_methods = $self->allowed_methods; + $c->response->headers(HTTP::Headers->new( + Allow => $allowed_methods->join(', '), + Accept_Post => 'application/hal+json; profile=http://purl.org/sipwise/ngcp-api/#rel-'.$self->resource_name, + )); + $c->response->content_type('application/json'); + $c->response->body(JSON::to_json({ methods => $allowed_methods })."\n"); + return; +} + +sub POST :Allow { + my ($self, $c) = @_; + + my $guard = $c->model('DB')->txn_scope_guard; + { + my $schema = $c->model('DB'); + my $resource = $self->get_valid_post_data( + c => $c, + media_type => 'application/json', + ); + last unless $resource; + + my $reseller_id; + if($c->user->roles eq "api_admin") { + } elsif($c->user->roles eq "api_reseller") { + $reseller_id = $c->user->reseller_id; + } else { + $reseller_id = $c->user->contract->contact->reseller_id; + } + + my $form = NGCP::Panel::Form::BillingFee->new; + my $billing_profile_id = $resource->{billing_profile_id} // undef; + $resource->{billing_zone_id} //= undef; + last unless $self->validate_form( + c => $c, + resource => $resource, + form => $form, + ); + $resource->{billing_profile_id} = $billing_profile_id; + + my $profile = $schema->resultset('billing_profiles')->find($resource->{billing_profile_id}); + unless($profile) { + $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Invalid 'billing_profile_id'."); + last; + } + if($c->user->roles ne "api_admin" && $profile->reseller_id != $reseller_id) { + $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Invalid 'billing_profile_id'."); + last; + } + my $zone = $profile->billing_zones->find($resource->{billing_zone_id}); + unless($zone) { + $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Invalid 'billing_zone_id'."); + last; + } + + my $fee; + try { + $fee = $profile->billing_fees->create($resource); + } catch($e) { + $c->log->error("failed to create billing fee: $e"); # TODO: user, message, trace, ... + $self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Failed to create billing fee."); + last; + } + + $guard->commit; + + $c->response->status(HTTP_CREATED); + $c->response->header(Location => sprintf('%s%d', $self->dispatch_path, $fee->id)); + $c->response->body(q()); + } + return; +} + +sub end : Private { + my ($self, $c) = @_; + + $self->log_response($c); +} + +# vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Controller/API/BillingFeesItem.pm b/lib/NGCP/Panel/Controller/API/BillingFeesItem.pm new file mode 100644 index 0000000000..99928edca9 --- /dev/null +++ b/lib/NGCP/Panel/Controller/API/BillingFeesItem.pm @@ -0,0 +1,192 @@ +package NGCP::Panel::Controller::API::BillingFeesItem; +use Sipwise::Base; +use namespace::sweep; +use HTTP::Headers qw(); +use HTTP::Status qw(:constants); +use MooseX::ClassAttribute qw(class_has); +use NGCP::Panel::Form::BillingFee qw(); +use NGCP::Panel::Utils::DateTime; +use NGCP::Panel::Utils::ValidateJSON qw(); +use Path::Tiny qw(path); +use Safe::Isa qw($_isa); +BEGIN { extends 'Catalyst::Controller::ActionRole'; } +require Catalyst::ActionRole::ACL; +require Catalyst::ActionRole::HTTPMethods; +require Catalyst::ActionRole::RequireSSL; + +with 'NGCP::Panel::Role::API'; +with 'NGCP::Panel::Role::API::BillingFees'; + +class_has('resource_name', is => 'ro', default => 'billingfees'); +class_has('dispatch_path', is => 'ro', default => '/api/billingfees/'); +class_has('relation', is => 'ro', default => 'http://purl.org/sipwise/ngcp-api/#rel-billingfees'); + +__PACKAGE__->config( + action => { + map { $_ => { + ACLDetachTo => '/api/root/invalid_user', + AllowedRole => 'api_admin', + Args => 1, + Does => [qw(ACL RequireSSL)], + Method => $_, + Path => __PACKAGE__->dispatch_path, + } } @{ __PACKAGE__->allowed_methods } + }, + action_roles => [qw(HTTPMethods)], +); + +sub auto :Private { + my ($self, $c) = @_; + + $self->set_body($c); + $self->log_request($c); +} + +sub GET :Allow { + my ($self, $c, $id) = @_; + { + last unless $self->valid_id($c, $id); + my $fee = $self->fee_by_id($c, $id); + last unless $self->resource_exists($c, billingfee => $fee); + + my $hal = $self->hal_from_fee($c, $fee); + + # TODO: we don't need reseller stuff here! + 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"|; + s/rel=self/rel="item self"/; + $_ + } $hal->http_headers), + ), $hal->as_json); + $c->response->headers($response->headers); + $c->response->body($response->content); + return; + } + return; +} + +sub HEAD :Allow { + my ($self, $c, $id) = @_; + $c->forward(qw(GET)); + $c->response->body(q()); + return; +} + +sub OPTIONS :Allow { + my ($self, $c, $id) = @_; + my $allowed_methods = $self->allowed_methods; + $c->response->headers(HTTP::Headers->new( + Allow => $allowed_methods->join(', '), + Accept_Patch => 'application/json-patch+json', + )); + $c->response->content_type('application/json'); + $c->response->body(JSON::to_json({ methods => $allowed_methods })."\n"); + return; +} + +sub PATCH :Allow { + my ($self, $c, $id) = @_; + my $guard = $c->model('DB')->txn_scope_guard; + { + my $preference = $self->require_preference($c); + last unless $preference; + + my $json = $self->get_valid_patch_data( + c => $c, + id => $id, + media_type => 'application/json-patch+json', + ); + last unless $json; + + my $fee = $self->fee_by_id($c, $id); + last unless $self->resource_exists($c, billingfee => $fee); + my $old_resource = { $fee->get_inflated_columns }; + my $resource = $self->apply_patch($c, $old_resource, $json); + last unless $resource; + + my $form = NGCP::Panel::Form::BillingFee->new; + $fee = $self->update_fee($c, $fee, $old_resource, $resource, $form); + last unless $fee; + + $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 $hal = $self->hal_from_fee($c, $fee, $form); + 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; +} + +sub PUT :Allow { + my ($self, $c, $id) = @_; + my $guard = $c->model('DB')->txn_scope_guard; + { + my $preference = $self->require_preference($c); + last unless $preference; + + my $fee = $self->fee_by_id($c, $id); + last unless $self->resource_exists($c, billingfee => $fee); + my $resource = $self->get_valid_put_data( + c => $c, + id => $id, + media_type => 'application/json', + ); + last unless $resource; + my $old_resource = { $fee->get_inflated_columns }; + + my $form = NGCP::Panel::Form::BillingFee->new; + $fee = $self->update_fee($c, $fee, $old_resource, $resource, $form); + last unless $fee; + + $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 $hal = $self->hal_from_fee($c, $fee, $form); + 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; +} + +sub DELETE :Allow { + my ($self, $c, $id) = @_; + my $guard = $c->model('DB')->txn_scope_guard; + { + my $fee = $self->fee_by_id($c, $id); + last unless $self->resource_exists($c, billingfee => $fee); + $fee->delete; + $guard->commit; + + $c->response->status(HTTP_NO_CONTENT); + $c->response->body(q()); + } + return; +} + +sub end : Private { + my ($self, $c) = @_; + + $self->log_response($c); +} + +# vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Controller/API/BillingProfiles.pm b/lib/NGCP/Panel/Controller/API/BillingProfiles.pm index d1d5b21eae..8cc1fbf0ab 100644 --- a/lib/NGCP/Panel/Controller/API/BillingProfiles.pm +++ b/lib/NGCP/Panel/Controller/API/BillingProfiles.pm @@ -50,8 +50,11 @@ sub GET :Allow { my $rows = $c->request->params->{rows} // 10; { my $profiles = $c->model('DB')->resultset('billing_profiles'); - if($c->user->roles eq "api_reseller") { + if($c->user->roles eq "api_admin") { + } elsif($c->user->roles eq "api_reseller") { $profiles = $profiles->search({ reseller_id => $c->user->reseller_id }); + } else { + $profiles = $profiles->search({ reseller_id => $c->user->contract->contact->reseller_id}); } my $total_count = int($profiles->count); $profiles = $profiles->search(undef, { diff --git a/lib/NGCP/Panel/Form/BillingFee.pm b/lib/NGCP/Panel/Form/BillingFee.pm index fae5ebaea6..e355aef8bd 100644 --- a/lib/NGCP/Panel/Form/BillingFee.pm +++ b/lib/NGCP/Panel/Form/BillingFee.pm @@ -52,7 +52,7 @@ has_field 'onpeak_init_rate' => ( precision => 18, element_attr => { rel => ['tooltip'], - title => ['The cost of the init interval in cents (e.g 0.90)'] + title => ['The cost of the init interval in cents per second (e.g 0.90)'] }, default => 0, ); @@ -64,6 +64,8 @@ has_field 'onpeak_init_interval' => ( title => ['The length of the first interval'] }, default => 60, + required => 1, + validate_method => \&validate_interval, ); has_field 'onpeak_follow_rate' => ( @@ -71,7 +73,7 @@ has_field 'onpeak_follow_rate' => ( precision => 18, element_attr => { rel => ['tooltip'], - title => ['The cost of each following interval in cents (e.g 0.90)'] + title => ['The cost of each following interval in cents per second (e.g 0.90)'] }, default => 0, ); @@ -83,6 +85,8 @@ has_field 'onpeak_follow_interval' => ( title => ['The length of the following intervals'] }, default => 60, + required => 1, + validate_method => \&validate_interval, ); has_field 'offpeak_init_rate' => ( @@ -90,7 +94,7 @@ has_field 'offpeak_init_rate' => ( precision => 18, element_attr => { rel => ['tooltip'], - title => ['The cost of the init interval in cents (e.g 0.90)'] + title => ['The cost of the init interval in cents per second (e.g 0.90)'] }, default => 0, ); @@ -102,6 +106,8 @@ has_field 'offpeak_init_interval' => ( title => ['The length of the first interval'] }, default => 60, + required => 1, + validate_method => \&validate_interval, ); has_field 'offpeak_follow_rate' => ( @@ -109,7 +115,7 @@ has_field 'offpeak_follow_rate' => ( precision => 18, element_attr => { rel => ['tooltip'], - title => ['The cost of each following interval in cents (e.g 0.90)'] + title => ['The cost of each following interval in cents per second (e.g 0.90)'] }, default => 0, ); @@ -121,6 +127,8 @@ has_field 'offpeak_follow_interval' => ( title => ['The length of the following intervals'] }, default => 60, + required => 1, + validate_method => \&validate_interval, ); has_field 'use_free_time' => ( @@ -155,5 +163,13 @@ has_block 'actions' => ( render_list => [qw/save/], ); +sub validate_interval { + my ($self, $field) = @_; + + if(int($field->value) < 1) { + $field->add_error("Invalid interval, must be bigger than 0"); + } +} + 1; # vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Role/API/BillingFees.pm b/lib/NGCP/Panel/Role/API/BillingFees.pm new file mode 100644 index 0000000000..a7d9ba8b5d --- /dev/null +++ b/lib/NGCP/Panel/Role/API/BillingFees.pm @@ -0,0 +1,122 @@ +package NGCP::Panel::Role::API::BillingFees; +use Moose::Role; +use Sipwise::Base; + +use boolean qw(true); +use Try::Tiny; +use Data::HAL qw(); +use Data::HAL::Link qw(); +use HTTP::Status qw(:constants); +use NGCP::Panel::Utils::DateTime; +use NGCP::Panel::Utils::Contract; +use NGCP::Panel::Form::BillingFee qw(); + +sub hal_from_fee { + my ($self, $c, $fee, $form) = @_; + + my %resource = $fee->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, $fee->id)), + Data::HAL::Link->new(relation => 'ngcp:billingprofiles', href => sprintf("/api/billingprofiles/%d", $fee->billing_profile->id)), + Data::HAL::Link->new(relation => 'ngcp:billingzones', href => sprintf("/api/billingzones/%d", $fee->billing_zone->id)), + ], + relation => 'ngcp:'.$self->resource_name, + ); + + $form //= NGCP::Panel::Form::BillingFee->new; + return unless $self->validate_form( + c => $c, + form => $form, + resource => \%resource, + run => 0, + ); + + $resource{id} = int($fee->id); + $resource{billing_profile_id} = int($fee->billing_profile_id); + $hal->resource({%resource}); + return $hal; +} + +sub fee_by_id { + my ($self, $c, $id) = @_; + + my $fees = $c->model('DB')->resultset('billing_fees'); + if($c->user->roles eq "api_admin") { + } elsif($c->user->roles eq "api_reseller") { + $fees = $fees->search({ + 'billing_zone.reseller_id' => $c->user->reseller_id, + }, { + join => 'billin_zone', + }); + } else { + $fees = $fees->search({ + 'billing_zone.reseller_id' => $c->user->contract->contact->reseller_id, + }, { + join => 'billin_zone', + }); + } + + return $fees->find($id); +} + +sub update_fee { + my ($self, $c, $fee, $old_resource, $resource, $form) = @_; + + my $reseller_id; + if($c->user->roles eq "api_admin") { + } elsif($c->user->roles eq "api_admin") { + $reseller_id = $c->user->reseller_id; + } else { + $reseller_id = $c->user->contract->contact->reseller_id; + } + $form //= NGCP::Panel::Form::BillingFee->new; + # TODO: for some reason, formhandler lets missing profile/zone id + my $billing_profile_id = $resource->{billing_profile_id} // undef; + $resource->{billing_zone_id} //= undef; + return unless $self->validate_form( + c => $c, + form => $form, + resource => $resource, + ); + $resource->{billing_profile_id} = $billing_profile_id; + + if($old_resource->{billing_profile_id} != $resource->{billing_profile_id}) { + my $profile = $c->model('DB')->resultset('billing_profiles') + ->find($resource->{billing_profile_id}); + unless($profile) { + $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Invalid 'billing_profile_id'"); + return; + } + if($c->user->roles ne "api_admin" && $profile->reseller->id != $reseller_id) { + $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Invalid 'billing_profile_id'"); + return; + } + } + + if($old_resource->{billing_zone_id} != $resource->{billing_zone_id}) { + my $zone = $c->model('DB')->resultset('billing_zones') + ->search(billing_profile_id => $resource->{billing_profile_id}) + ->find($resource->{billing_zone_id}); + unless($zone) { + $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Invalid 'billing_zone_id'"); + return; + } + } + + $fee->update($resource); + + return $fee; +} + +1; +# vim: set tabstop=4 expandtab: