From 63056e4d15c660a5aed3c285f708a3b13f08b7ed Mon Sep 17 00:00:00 2001 From: Andreas Granig Date: Thu, 10 Jul 2014 13:24:49 +0200 Subject: [PATCH] MT#7867 Also add invoice tmpl per customer in API --- lib/NGCP/Panel/Controller/API/Customers.pm | 5 + .../Panel/Controller/API/InvoiceTemplates.pm | 144 ++++++++++++++++++ .../Controller/API/InvoiceTemplatesItem.pm | 91 +++++++++++ lib/NGCP/Panel/Form/Contract/Basic.pm | 2 +- .../Panel/Form/Contract/ProductOptional.pm | 2 +- lib/NGCP/Panel/Role/API/Customers.pm | 12 ++ lib/NGCP/Panel/Role/API/InvoiceTemplates.pm | 107 +++++++++++++ 7 files changed, 361 insertions(+), 2 deletions(-) create mode 100644 lib/NGCP/Panel/Controller/API/InvoiceTemplates.pm create mode 100644 lib/NGCP/Panel/Controller/API/InvoiceTemplatesItem.pm create mode 100644 lib/NGCP/Panel/Role/API/InvoiceTemplates.pm diff --git a/lib/NGCP/Panel/Controller/API/Customers.pm b/lib/NGCP/Panel/Controller/API/Customers.pm index 7addba8c6b..1caad3d2bf 100644 --- a/lib/NGCP/Panel/Controller/API/Customers.pm +++ b/lib/NGCP/Panel/Controller/API/Customers.pm @@ -235,6 +235,11 @@ sub POST :Allow { $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "The reseller of the contact doesn't match the reseller of the billing profile"); last; } + if($customer->invoice_template_id && + $customer->invoice_template->reseller_id != $customer->contact->reseller_id) { + $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Invalid 'invoice_template_id', doesn't exist for reseller assigned to customer contact"); + return; + } if($customer->subscriber_email_template_id && $customer->subscriber_email_template->reseller_id != $customer->contact->reseller_id) { $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Invalid 'subscriber_email_template_id', doesn't exist for reseller assigned to customer contact"); diff --git a/lib/NGCP/Panel/Controller/API/InvoiceTemplates.pm b/lib/NGCP/Panel/Controller/API/InvoiceTemplates.pm new file mode 100644 index 0000000000..a2baa04663 --- /dev/null +++ b/lib/NGCP/Panel/Controller/API/InvoiceTemplates.pm @@ -0,0 +1,144 @@ +package NGCP::Panel::Controller::API::InvoiceTemplates; +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 Path::Tiny qw(path); +use Safe::Isa qw($_isa); +BEGIN { extends 'Catalyst::Controller::ActionRole'; } +require Catalyst::ActionRole::ACL; +require Catalyst::ActionRole::CheckTrailingSlash; +require Catalyst::ActionRole::HTTPMethods; +require Catalyst::ActionRole::RequireSSL; + +class_has 'api_description' => ( + is => 'ro', + isa => 'Str', + default => + 'Defines invoice templates used to generate customer invoices. Only returns meta data at this point.', +); + +class_has 'query_params' => ( + is => 'ro', + isa => 'ArrayRef', + default => sub {[ + { + param => 'reseller_id', + description => 'Filter for invoice templates belonging to a specific reseller', + query => { + first => sub { + my $q = shift; + { reseller_id => $q }; + }, + second => sub {}, + }, + }, + ]}, +); + +with 'NGCP::Panel::Role::API::InvoiceTemplates'; + +class_has('resource_name', is => 'ro', default => 'invoicetemplates'); +class_has('dispatch_path', is => 'ro', default => '/api/invoicetemplates/'); +class_has('relation', is => 'ro', default => 'http://purl.org/sipwise/ngcp-api/#rel-invoicetemplates'); + +__PACKAGE__->config( + action => { + map { $_ => { + ACLDetachTo => '/api/root/invalid_user', + AllowedRole => [qw/admin reseller/], + 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 $items = $self->item_rs($c); + (my $total_count, $items) = $self->paginate_order_collection($c, $items); + my (@embedded, @links); + my $form = $self->get_form($c); + for my $item ($items->search({}, {prefetch => ['reseller']})->all) { + push @embedded, $self->hal_from_item($c, $item, $form); + push @links, Data::HAL::Link->new( + relation => 'ngcp:'.$self->resource_name, + href => sprintf('/%s%d', $c->request->path, $item->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 $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; +} + +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 end : Private { + my ($self, $c) = @_; + + $self->log_response($c); +} + +# vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Controller/API/InvoiceTemplatesItem.pm b/lib/NGCP/Panel/Controller/API/InvoiceTemplatesItem.pm new file mode 100644 index 0000000000..555f0a2944 --- /dev/null +++ b/lib/NGCP/Panel/Controller/API/InvoiceTemplatesItem.pm @@ -0,0 +1,91 @@ +package NGCP::Panel::Controller::API::InvoiceTemplatesItem; +use Sipwise::Base; +use namespace::sweep; +use HTTP::Headers qw(); +use HTTP::Status qw(:constants); +use MooseX::ClassAttribute qw(class_has); +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::InvoiceTemplates'; + +class_has('resource_name', is => 'ro', default => 'invoicetemplates'); +class_has('dispatch_path', is => 'ro', default => '/api/invoicetemplates/'); +class_has('relation', is => 'ro', default => 'http://purl.org/sipwise/ngcp-api/#rel-invoicetemplates'); + +__PACKAGE__->config( + action => { + map { $_ => { + ACLDetachTo => '/api/root/invalid_user', + AllowedRole => [qw/admin reseller/], + 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 $item = $self->item_by_id($c, $id); + last unless $self->resource_exists($c, invoicetemplate => $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"|; + 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 end : Private { + my ($self, $c) = @_; + + $self->log_response($c); +} + +# vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Form/Contract/Basic.pm b/lib/NGCP/Panel/Form/Contract/Basic.pm index a5eb9e6628..7a2b3496a5 100644 --- a/lib/NGCP/Panel/Form/Contract/Basic.pm +++ b/lib/NGCP/Panel/Form/Contract/Basic.pm @@ -95,7 +95,7 @@ has_field 'invoice_template' => ( required => 0, element_attr => { rel => ['tooltip'], - title => ['The invoice template for invoice generation.'] + title => ['The invoice template for invoice generation. If none is assigned, no invoice will be generated for this customer.'] }, ); diff --git a/lib/NGCP/Panel/Form/Contract/ProductOptional.pm b/lib/NGCP/Panel/Form/Contract/ProductOptional.pm index 0967473430..9abad8e0df 100644 --- a/lib/NGCP/Panel/Form/Contract/ProductOptional.pm +++ b/lib/NGCP/Panel/Form/Contract/ProductOptional.pm @@ -22,7 +22,7 @@ has_field 'max_subscribers' => ( has_block 'fields' => ( tag => 'div', class => [qw/modal-body/], - render_list => [qw/contact billing_profile product max_subscribers status external_id subscriber_email_template passreset_email_template invoice_email_template vat_rate add_vat/], + render_list => [qw/contact billing_profile product max_subscribers status external_id invoice_template subscriber_email_template passreset_email_template invoice_email_template vat_rate add_vat/], ); 1; diff --git a/lib/NGCP/Panel/Role/API/Customers.pm b/lib/NGCP/Panel/Role/API/Customers.pm index 8943375253..39dce2e062 100644 --- a/lib/NGCP/Panel/Role/API/Customers.pm +++ b/lib/NGCP/Panel/Role/API/Customers.pm @@ -79,6 +79,7 @@ sub hal_from_customer { Data::HAL::Link->new(relation => 'ngcp:customerpreferences', href => sprintf("/api/customerpreferences/%d", $customer->id)), Data::HAL::Link->new(relation => 'ngcp:billingprofiles', href => sprintf("/api/billingprofiles/%d", $billing_profile_id)), Data::HAL::Link->new(relation => 'ngcp:contractbalances', href => sprintf("/api/contractbalances/%d", $contract_balance->id)), + $customer->invoice_template ? (Data::HAL::Link->new(relation => 'ngcp:invoicetemplates', href => sprintf("/api/invoicetemplates/%d", $customer->invoice_template_id))) : (), $customer->subscriber_email_template_id ? (Data::HAL::Link->new(relation => 'ngcp:subscriberemailtemplates', href => sprintf("/api/emailtemplates/%d", $customer->subscriber_email_template_id))) : (), $customer->passreset_email_template_id ? (Data::HAL::Link->new(relation => 'ngcp:passresetemailtemplates', href => sprintf("/api/emailtemplates/%d", $customer->passreset_email_template_id))) : (), $customer->invoice_email_template_id ? (Data::HAL::Link->new(relation => 'ngcp:invoiceemailtemplates', href => sprintf("/api/emailtemplates/%d", $customer->invoice_email_template_id))) : (), @@ -170,6 +171,17 @@ sub update_customer { $custcontact = $customer->contact; } + my $oldinvoicetmpl = $old_resource->{invoice_template_id} // 0; + if($resource->{invoice_template_id} && + $oldinvoicetmpl != $resource->{invoice_template_id}) { + my $tmpl = $c->model('DB')->resultset('invoice_templates') + ->search({ reseller_id => $custcontact->reseller_id }) + ->find($resource->{invoice_template_id}); + unless($tmpl) { + $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Invalid 'invoice_template_id', doesn't exist for reseller assigned to customer contact"); + return; + } + } my $oldsubtmpl = $old_resource->{subscriber_email_template_id} // 0; if($resource->{subscriber_email_template_id} && $oldsubtmpl != $resource->{subscriber_email_template_id}) { diff --git a/lib/NGCP/Panel/Role/API/InvoiceTemplates.pm b/lib/NGCP/Panel/Role/API/InvoiceTemplates.pm new file mode 100644 index 0000000000..9e7c0165ad --- /dev/null +++ b/lib/NGCP/Panel/Role/API/InvoiceTemplates.pm @@ -0,0 +1,107 @@ +package NGCP::Panel::Role::API::InvoiceTemplates; +use Moose::Role; +use Sipwise::Base; +with 'NGCP::Panel::Role::API' => { + -alias =>{ item_rs => '_item_rs', }, + -excludes => [ 'item_rs' ], +}; + +use boolean qw(true); +use TryCatch; +use Data::HAL qw(); +use Data::HAL::Link qw(); +use HTTP::Status qw(:constants); +use NGCP::Panel::Form::Invoice::TemplateAdmin; +use NGCP::Panel::Form::Invoice::TemplateReseller; + +sub item_rs { + my ($self, $c) = @_; + + my $item_rs = $c->model('DB')->resultset('invoice_templates'); + if($c->user->roles eq "admin") { + } elsif($c->user->roles eq "reseller") { + $item_rs = $item_rs->search({ reseller_id => $c->user->reseller_id }); + } + return $item_rs; +} + +sub get_form { + my ($self, $c) = @_; + if($c->user->roles eq "admin") { + return NGCP::Panel::Form::Invoice::TemplateAdmin->new; + } elsif($c->user->roles eq "reseller") { + return NGCP::Panel::Form::Invoice::TemplateReseller->new; + } +} + +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)), + Data::HAL::Link->new(relation => 'ngcp:resellers', href => sprintf("/api/resellers/%d", $item->reseller_id)), + ], + relation => 'ngcp:'.$self->resource_name, + ); + + $form //= $self->get_form($c); + + $self->validate_form( + c => $c, + resource => \%resource, + form => $form, + run => 0, + ); + + $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); + return $item_rs->find($id); +} + +sub update_item { + my ($self, $c, $item, $old_resource, $resource, $form) = @_; + + $form //= $self->get_form($c); + return unless $self->validate_form( + c => $c, + form => $form, + resource => $resource, + ); + if($c->user->roles eq "admin") { + } elsif($c->user->roles eq "reseller") { + $resource->{reseller_id} = $c->user->reseller_id; + } + + my $dup_item = $c->model('DB')->resultset('invoice_templates')->find({ + reseller_id => $resource->{reseller_id}, + name => $resource->{name}, + }); + if($dup_item && $dup_item->id != $item->id) { + $c->log->error("invoice template with name '$$resource{name}' already exists for reseller_id '$$resource{reseller_id}'"); # TODO: user, message, trace, ... + $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Invoice template with this name already exists for this reseller"); + return; + } + + $item->update($resource); + + return $item; +} + +1; +# vim: set tabstop=4 expandtab: