diff --git a/Build.PL b/Build.PL index 1804139b18..91532975ce 100644 --- a/Build.PL +++ b/Build.PL @@ -34,10 +34,8 @@ my $builder = Local::Module::Build->new( 'Catalyst::Runtime' => '5.90040', 'Catalyst::View::JSON' => 0, 'Catalyst::View::TT' => 0, - 'CHI' => 0, 'Config::General' => 0, 'Data::HAL' => 0, - 'Data::Record' => 0, 'Convert::Ascii85' => 0, 'Data::Dumper' => 0, 'Data::Validate::IP' => 0, @@ -46,7 +44,6 @@ my $builder = Local::Module::Build->new( 'DateTime::Format::ISO8601' => 0, 'DateTime::Format::RFC3339' => 0, 'DBIx::Class::ResultSet::RecursiveUpdate' => '0.30', - 'Digest::SHA3' => 0, 'Email::Valid' => 0, 'File::ShareDir' => 0, 'File::Type' => 0, diff --git a/lib/NGCP/Panel.pm b/lib/NGCP/Panel.pm index 390808d343..28352c128d 100644 --- a/lib/NGCP/Panel.pm +++ b/lib/NGCP/Panel.pm @@ -25,8 +25,6 @@ use Catalyst qw/ Session::Store::FastMmap Session::State::Cookie /; -use CHI qw(); -require CHI::Driver::FastMmap; use Log::Log4perl::Catalyst qw(); use NGCP::Panel::Cache::Serializer qw(); use NGCP::Panel::Middleware::HSTS qw(); @@ -171,16 +169,6 @@ __PACKAGE__->config( default_view => 'HTML' ); __PACKAGE__->log(Log::Log4perl::Catalyst->new($logger_config)); -has('cache', is => 'ro', default => sub { - my ($self) = @_; - return CHI->new( - cache_size => '30m', - driver => 'FastMmap', - root_dir => $self->config->{cache_root}, - serializer => NGCP::Panel::Cache::Serializer->new, - ); -}); - # Start the application __PACKAGE__->setup(); diff --git a/lib/NGCP/Panel/Controller/API/Contracts.pm b/lib/NGCP/Panel/Controller/API/Contracts.pm index f10bd9f307..ff07531ab2 100644 --- a/lib/NGCP/Panel/Controller/API/Contracts.pm +++ b/lib/NGCP/Panel/Controller/API/Contracts.pm @@ -4,17 +4,11 @@ use namespace::sweep; use boolean qw(true); use Data::HAL qw(); use Data::HAL::Link qw(); -use Data::Record qw(); -use DateTime::Format::HTTP qw(); -use DateTime::Format::RFC3339 qw(); -use Digest::SHA3 qw(sha3_256_base64); use HTTP::Headers qw(); use HTTP::Status qw(:constants); -use JSON qw(); use MooseX::ClassAttribute qw(class_has); -use NGCP::Panel::Utils::ValidateJSON qw(); +use NGCP::Panel::Form::Contract::PeeringReseller qw(); use Path::Tiny qw(path); -use Regexp::Common qw(delimited); # $RE{delimited} use Safe::Isa qw($_isa); BEGIN { extends 'Catalyst::Controller::ActionRole'; } require Catalyst::ActionRole::ACL; @@ -24,6 +18,7 @@ require Catalyst::ActionRole::RequireSSL; with 'NGCP::Panel::Role::API'; +class_has('resource_name', is => 'ro', default => 'contracts'); class_has('dispatch_path', is => 'ro', default => '/api/contracts/'); class_has('relation', is => 'ro', default => 'http://purl.org/sipwise/ngcp-api/#rel-contracts'); @@ -48,25 +43,29 @@ sub auto :Private { $self->log_request($c); } -sub GET :Allow :Args(0) { +sub GET :Allow { my ($self, $c) = @_; my $page = $c->request->params->{page} // 1; my $rows = $c->request->params->{rows} // 10; { - last if $self->cached($c); - my $contracts = $c->model('DB')->resultset('contracts'); - $self->last_modified($contracts->get_column('modify_timestamp')->max_rs->single->modify_timestamp); + my $contracts = $c->model('DB')->resultset('contracts') + ->search({ + 'contact.reseller_id' => undef + },{ + join => 'contact' + }); my $total_count = int($contracts->count); $contracts = $contracts->search(undef, { page => $page, rows => $rows, }); my (@embedded, @links); + my $form = NGCP::Panel::Form::Contract::PeeringReseller->new; for my $contract ($contracts->search({}, {order_by => {-asc => 'me.id'}, prefetch => ['contact']})->all) { - push @embedded, $self->hal_from_contract($contract); + push @embedded, $self->hal_from_contract($c, $contract, $form); push @links, Data::HAL::Link->new( - relation => 'ngcp:contracts', - href => sprintf('/api/contracts/%d', $contract->id), + relation => 'ngcp:'.$self->resource_name, + href => sprintf('/%s%d', $c->request->path, $contract->id), ); } push @links, @@ -77,14 +76,15 @@ sub GET :Allow :Args(0) { templated => true, ), Data::HAL::Link->new(relation => 'profile', href => 'http://purl.org/sipwise/ngcp-api/'), - Data::HAL::Link->new(relation => 'self', href => "/api/contracts/?page=$page&rows=$rows"); - + 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 => "/api/contracts/?page=".($page+1)."&rows=$rows"), + 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 => "/api/contracts/?page=".($page-1)."&rows=$rows"); + 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], @@ -92,19 +92,14 @@ sub GET :Allow :Args(0) { $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-contracts)"|rel="item $1"|; + s|rel="(http://purl.org/sipwise/ngcp-api/#rel-$rname)"|rel="item $1"|; s/rel=self/rel="collection self"/; $_ } $hal->http_headers), - $hal->http_headers, - Cache_Control => 'no-cache, private', - ETag => $self->etag($hal->as_json), - Expires => DateTime::Format::HTTP->format_datetime($self->expires), - Last_Modified => DateTime::Format::HTTP->format_datetime($self->last_modified), ), $hal->as_json); - $c->cache->set($c->request->uri->canonical->as_string, $response, { expires_at => $self->expires->epoch }); $c->response->headers($response->headers); $c->response->body($response->content); return; @@ -112,74 +107,75 @@ sub GET :Allow :Args(0) { return; } -sub HEAD : Allow { +sub HEAD :Allow { my ($self, $c) = @_; $c->forward(qw(GET)); $c->response->body(q()); return; } -sub OPTIONS : Allow { +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-contracts', - Content_Language => 'en', + 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 { +sub POST :Allow { my ($self, $c) = @_; + + my $guard = $c->model('DB')->txn_scope_guard; { my $resource = $self->get_valid_post_data( - c => $c, + c => $c, media_type => 'application/json', ); - last unless $resource; + last unless $resource; - # this is only accessible by admins, so on other roles check - my $contract_form = NGCP::Panel::Form::Contract::PeeringReseller->new; + my $form = NGCP::Panel::Form::Contract::PeeringReseller->new; last unless $self->validate_form( c => $c, resource => $resource, - form => $contract_form, + form => $form, ); - my $contact = $c->model('DB')->resultset('contacts')->find($resource->{contact_id}); - unless($contact) { - $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Invalid contact_id."); # TODO: log error, ... - last; - } - if($contact->reseller_id) { - # TODO: should be allow to create customer contracts here as well? If not, reject - # a contact with a reseller! - #$self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Invalid contact for system contract, must not belong to a reseller."); # TODO: log error, ... - #last; - } - - # TODO: do we need to use our DateTime utils for localized "now"? my $now = DateTime->now; $resource->{create_timestamp} = $now; $resource->{modify_timestamp} = $now; - my $contract = $c->model('DB')->resultset('contracts')->create($resource); + my $contract; + try { + $contract = $c->model('DB')->resultset('contracts')->create($resource); + + # TODO: billing_mappings, contract_balances, ... + # TODO: product (sippeering or reseller) + } catch($e) { + $c->log->error("failed to create contract: $e"); # TODO: user, message, trace, ... + $self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Failed to create contract."); + last; + } + + $guard->commit; - $c->cache->remove($c->request->uri->canonical->as_string); $c->response->status(HTTP_CREATED); - $c->response->header(Location => sprintf('/api/contracts/%d', $contract->id)); + $c->response->header(Location => sprintf('/%s%d', $c->request->path, $contract->id)); $c->response->body(q()); } return; } sub hal_from_contract : Private { - my ($self, $contract) = @_; - # XXX invalid 00-00-00 dates + my ($self, $c, $contract, $form) = @_; + + # TODO: fixxxxxxme + my $billing_profile_id = 1; + my $contract_balance_id = 1; + my %resource = $contract->get_inflated_columns; - my $id = delete $resource{id}; my $hal = Data::HAL->new( links => [ @@ -189,37 +185,27 @@ sub hal_from_contract : Private { name => 'ngcp', templated => true, ), - Data::HAL::Link->new(relation => 'collection', href => '/api/contracts/'), + Data::HAL::Link->new(relation => 'collection', href => sprintf('/%s', $c->request->path)), Data::HAL::Link->new(relation => 'profile', href => 'http://purl.org/sipwise/ngcp-api/'), - Data::HAL::Link->new(relation => 'self', href => "/api/contracts/$id"), - $contract->contact - ? Data::HAL::Link->new( - relation => 'ngcp:contacts', - href => sprintf('/api/contacts/%d', $contract->contact_id), - ) : (), + Data::HAL::Link->new(relation => 'self', href => sprintf("/%s%d", $c->request->path, $contract->id)), + Data::HAL::Link->new(relation => 'ngcp:systemcontacts', href => sprintf("/api/systemcontacts/%d", $contract->contact->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)), ], - relation => 'ngcp:contracts', + relation => 'ngcp:'.$self->resource_name, + ); + + return unless $self->validate_form( + c => $c, + form => $form, + resource => \%resource, + run => 0, ); - my %fields = map { $_ => undef } qw(external_id status); - for my $k (keys %resource) { - delete $resource{$k} unless exists $fields{$k}; - $resource{$k} = DateTime::Format::RFC3339->format_datetime($resource{$k}) if $resource{$k}->$_isa('DateTime'); - } $hal->resource({%resource}); return $hal; } -sub valid_id : Private { - my ($self, $c, $id) = @_; - return 1 if $id->is_integer; - $c->response->status(HTTP_BAD_REQUEST); - $c->response->header('Content-Language' => 'en'); - $c->response->content_type('application/xhtml+xml'); - $c->stash(template => 'api/invalid_query_parameter.tt', key => 'id'); - return; -} - sub end : Private { my ($self, $c) = @_; diff --git a/lib/NGCP/Panel/Controller/API/ContractsItem.pm b/lib/NGCP/Panel/Controller/API/ContractsItem.pm index 11e10da6eb..d8dcb5947d 100644 --- a/lib/NGCP/Panel/Controller/API/ContractsItem.pm +++ b/lib/NGCP/Panel/Controller/API/ContractsItem.pm @@ -4,19 +4,13 @@ use namespace::sweep; use boolean qw(true); use Data::HAL qw(); use Data::HAL::Link qw(); -use Data::Record qw(); -use DateTime::Format::HTTP qw(); -use DateTime::Format::RFC3339 qw(); -use Digest::SHA3 qw(sha3_256_base64); use HTTP::Headers qw(); use HTTP::Status qw(:constants); -use JSON qw(); use MooseX::ClassAttribute qw(class_has); +use NGCP::Panel::Form::Contract::PeeringReseller qw(); use NGCP::Panel::Utils::ValidateJSON qw(); use Path::Tiny qw(path); -use Regexp::Common qw(delimited); # $RE{delimited} use Safe::Isa qw($_isa); -use Types::Standard qw(InstanceOf); BEGIN { extends 'Catalyst::Controller::ActionRole'; } require Catalyst::ActionRole::ACL; require Catalyst::ActionRole::HTTPMethods; @@ -24,9 +18,9 @@ require Catalyst::ActionRole::RequireSSL; with 'NGCP::Panel::Role::API'; +class_has('resource_name', is => 'ro', default => 'contracts'); class_has('dispatch_path', is => 'ro', default => '/api/contracts/'); class_has('relation', is => 'ro', default => 'http://purl.org/sipwise/ngcp-api/#rel-contracts'); -has('last_modified', is => 'rw', isa => InstanceOf['DateTime']); __PACKAGE__->config( action => { @@ -53,22 +47,19 @@ sub GET :Allow { my ($self, $c, $id) = @_; { last unless $self->valid_id($c, $id); - last if $self->cached($c); my $contract = $self->contract_by_id($c, $id); last unless $self->resource_exists($c, contract => $contract); - my $hal = $self->hal_from_contract($c, $contract); + + my $form = NGCP::Panel::Form::Contract::PeeringReseller->new; + my $hal = $self->hal_from_contract($c, $contract, $form); + 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-contacts)"|rel="item $1"|; + s|rel="(http://purl.org/sipwise/ngcp-api/#rel-resellers)"|rel="item $1"|; s/rel=self/rel="item self"/; $_ } $hal->http_headers), - Cache_Control => 'no-cache, private', - ETag => $self->etag($hal->as_json), - Expires => DateTime::Format::HTTP->format_datetime($self->expires), - Last_Modified => DateTime::Format::HTTP->format_datetime($self->last_modified), ), $hal->as_json); - $c->cache->set($c->request->uri->canonical->as_string, $response, { expires_at => $self->expires->epoch }); $c->response->headers($response->headers); $c->response->body($response->content); return; @@ -89,7 +80,6 @@ sub OPTIONS :Allow { $c->response->headers(HTTP::Headers->new( Allow => $allowed_methods->join(', '), Accept_Patch => 'application/json-patch+json', - Content_Language => 'en', )); $c->response->content_type('application/json'); $c->response->body(JSON::to_json({ methods => $allowed_methods })."\n"); @@ -98,107 +88,50 @@ sub OPTIONS :Allow { sub PATCH :Allow { my ($self, $c, $id) = @_; - my $media_type = 'application/json-patch+json'; my $guard = $c->model('DB')->txn_scope_guard; { - last unless $self->valid_id($c, $id); - last unless $self->forbid_link_header($c); - last unless $self->valid_media_type($c, $media_type); - last unless $self->require_precondition($c, 'If-Match'); my $preference = $self->require_preference($c); last unless $preference; - my $cached = $c->cache->get($c->request->uri->canonical->as_string); - my ($contract, $entity); - if ($cached) { - try { - die 'not a response object' unless $cached->$_isa('HTTP::Response'); - } catch($e) { - die "cache poisoned: $e"; - }; - last unless $self->valid_precondition($c, $cached->header('ETag'), 'contract'); - try { - NGCP::Panel::Utils::ValidateJSON->new($cached->content); - $entity = JSON::decode_json($cached->content); - } catch($e) { - die "cache poisoned: $e"; - }; - } else { - if ('*' eq $c->request->header('If-Match')) { - $contract = $self->contract_by_id($c, $id); - last unless $self->resource_exists($c, contract => $contract); - $entity = JSON::decode_json($self->hal_from_contract($c, $contract)->as_json); - } else { - $self->error($c, HTTP_PRECONDITION_FAILED, "This 'contract' entity cannot be found, it is either expired or does not exist. Fetch a fresh one."); - last; - } - } - last unless $self->require_body($c); - my $json = $c->stash->{body}; - last unless $self->require_wellformed_json($c, $media_type, $json); - last unless $self->require_valid_patch($c, $json); - $entity = $self->apply_patch($c, $entity, $json); - last unless $entity; - last unless $self->valid_entity($c, $entity); - - my $hal = Data::HAL->from_json( - JSON::to_json($entity, { canonical => 1, convert_blessed => 1, pretty => 1, utf8 => 1 }) + my $json = $self->get_valid_patch_data( + c => $c, + id => $id, + media_type => 'application/json-patch+json', ); + last unless $json; - my $contact_id; - { - my $contact_link = ($hal->links // [])->grep(sub { - $_->relation->eq('http://purl.org/sipwise/ngcp-api/#rel-contacts') - }); - - if ($contact_link->size) { - my $contact_uri = URI->new_abs($contact_link->at(0)->href->as_string, $c->req->uri)->canonical; - my $contacts_uri = URI->new_abs('/api/contacts/', $c->req->uri)->canonical; - if (0 != index $contact_uri, $contacts_uri) { - $c->response->status(HTTP_UNPROCESSABLE_ENTITY); - $c->response->header('Content-Language' => 'en'); - $c->response->content_type('application/xhtml+xml'); - $c->stash( - template => 'api/unprocessable_entity.tt', - error_message => "The link $contact_uri cannot express a contact relationship.", - ); - last; - } - $contact_id = $contact_uri->rel($contacts_uri)->query_param('id'); - last unless $self->valid_id($c, $contact_id); - } - } - my $resource = $hal->resource; + my $contract = $self->contract_by_id($c, $id); + last unless $self->resource_exists($c, contract => $contract); + my $resource = { $contract->get_inflated_columns }; + $resource = $self->apply_patch($c, $resource, $json); + last unless $resource; - my %fields = map { $_ => undef } qw(external_id status); - for my $k (keys %{ $resource }) { - delete $resource->{$k} unless exists $fields{$k}; - $resource->{$k} = DateTime::Format::RFC3339->format_datetime($resource->{$k}) - if $resource->{$k}->$_isa('DateTime'); - } + my $form = NGCP::Panel::Form::Contract::PeeringReseller->new; + last unless $self->validate_form( + c => $c, + form => $form, + resource => $resource + ); - $resource->{contact_id} = $contact_id; $resource->{modify_timestamp} = DateTime->now; - $contract = $self->contract_by_id($c, $id) unless $contract; $contract->update($resource); + + # TODO: what about changed product, billing-profile etc? + # TODO: guess we need to update billing-mappings also + # TODO: handle termination, .... + $guard->commit; if ('minimal' eq $preference) { - $c->cache->remove($c->request->uri->canonical->as_string); $c->response->status(HTTP_NO_CONTENT); $c->response->header(Preference_Applied => 'return=minimal'); $c->response->body(q()); } else { - $hal = $self->hal_from_contract($c, $contract); + my $hal = $self->hal_from_contract($c, $contract); my $response = HTTP::Response->new(HTTP_OK, undef, HTTP::Headers->new( $hal->http_headers, - Cache_Control => 'no-cache, private', - ETag => $self->etag($hal->as_json), - Expires => DateTime::Format::HTTP->format_datetime($self->expires), - Last_Modified => DateTime::Format::HTTP->format_datetime($self->last_modified), ), $hal->as_json); - $c->cache->set($c->request->uri->canonical->as_string, $response, { expires_at => $self->expires->epoch }); $c->response->headers($response->headers); - $c->response->header(Preference_Applied => 'return=representation'); # don't cache this + $c->response->header(Preference_Applied => 'return=representation'); $c->response->body($response->content); } } @@ -207,119 +140,99 @@ sub PATCH :Allow { sub PUT :Allow { my ($self, $c, $id) = @_; - my $media_type = 'application/hal+json'; my $guard = $c->model('DB')->txn_scope_guard; { - last unless $self->valid_id($c, $id); - last unless $self->forbid_link_header($c); - last unless $self->valid_media_type($c, $media_type); - last unless $self->require_precondition($c, 'If-Match'); + my $resource = $self->get_valid_put_data( + c => $c, + id => $id, + media_type => 'application/json', + ); + last unless $resource; my $preference = $self->require_preference($c); last unless $preference; - my $cached = $c->cache->get($c->request->uri->canonical->as_string); - my ($contract, $entity); - if ($cached) { - try { - die 'not a response object' unless $cached->$_isa('HTTP::Response'); - } catch($e) { - die "cache poisoned: $e"; - }; - last unless $self->valid_precondition($c, $cached->header('ETag'), 'contract'); - try { - NGCP::Panel::Utils::ValidateJSON->new($cached->content); - $entity = JSON::decode_json($cached->content); - } catch($e) { - die "cache poisoned: $e"; - }; - } else { - if ('*' eq $c->request->header('If-Match')) { - $contract = $self->contract_by_id($c, $id); - last unless $self->resource_exists($c, contract => $contract); - $entity = JSON::decode_json($self->hal_from_contract($c, $contract)->as_json); - } else { - $self->error($c, HTTP_PRECONDITION_FAILED, "This 'contract' entity cannot be found, it is either expired or does not exist. Fetch a fresh one."); - last; - } - } - last unless $self->require_body($c); - my $json = $c->stash->{body}; - last unless $self->require_wellformed_json($c, $media_type, $json); - $entity = JSON::decode_json($json); - last unless $self->valid_entity($c, $entity); - my $hal = Data::HAL->from_json($json); - - my $contact_id; - { - my $contact_link = ($hal->links // [])->grep(sub { - $_->relation->eq('http://purl.org/sipwise/ngcp-api/#rel-contacts') - }); - - if ($contact_link->size) { - my $contact_uri = URI->new_abs($contact_link->at(0)->href->as_string, $c->req->uri)->canonical; - my $contacts_uri = URI->new_abs('/api/contacts/', $c->req->uri)->canonical; - if (0 != index $contact_uri, $contacts_uri) { - $c->response->status(HTTP_UNPROCESSABLE_ENTITY); - $c->response->header('Content-Language' => 'en'); - $c->response->content_type('application/xhtml+xml'); - $c->stash( - template => 'api/unprocessable_entity.tt', - error_message => "The link $contact_uri cannot express a contact relationship.", - ); - last; - } - $contact_id = $contact_uri->rel($contacts_uri)->query_param('id'); - last unless $self->valid_id($c, $contact_id); - } - } - my $resource = $hal->resource; - my %fields = map { $_ => undef } qw(external_id status); - for my $k (keys %{ $resource }) { - delete $resource->{$k} unless exists $fields{$k}; - $resource->{$k} = DateTime::Format::RFC3339->format_datetime($resource->{$k}) - if $resource->{$k}->$_isa('DateTime'); - } + my $form = NGCP::Panel::Form::Contract::PeeringReseller->new; + last unless $self->validate_form( + c => $c, + resource => $resource, + form => $form, + ); - $resource->{contact_id} = $contact_id; $resource->{modify_timestamp} = DateTime->now; - $contract = $self->contract_by_id($c, $id) unless $contract; + my $contract = $self->contract_by_id($c, $id); + $contract->update($resource); + # TODO: again, billing-mappings etc + + # TODO: handle termination, .... + # TODO: handle termination, .... + $guard->commit; if ('minimal' eq $preference) { - $c->cache->remove($c->request->uri->canonical->as_string); $c->response->status(HTTP_NO_CONTENT); $c->response->header(Preference_Applied => 'return=minimal'); $c->response->body(q()); } else { - $hal = $self->hal_from_contract($c, $contract); + my $hal = $self->hal_from_contract($c, $contract, $form); my $response = HTTP::Response->new(HTTP_OK, undef, HTTP::Headers->new( $hal->http_headers, - Cache_Control => 'no-cache, private', - ETag => $self->etag($hal->as_json), - Expires => DateTime::Format::HTTP->format_datetime($self->expires), - Last_Modified => DateTime::Format::HTTP->format_datetime($self->last_modified), ), $hal->as_json); - $c->cache->set($c->request->uri->canonical->as_string, $response, { expires_at => $self->expires->epoch }); $c->response->headers($response->headers); - $c->response->header(Preference_Applied => 'return=representation'); # don't cache this + $c->response->header(Preference_Applied => 'return=representation'); $c->response->body($response->content); } } return; } -sub contract_by_id :Private { +sub DELETE :Allow { my ($self, $c, $id) = @_; - return $c->model('DB')->resultset('contracts')->find({'me.id' => $id}, {prefetch => ['contact']}); + my $guard = $c->model('DB')->txn_scope_guard; + { + my $contract = $self->contract_by_id($c, $id); + last unless $self->resource_exists($c, contract => $contract); + + # TODO: do we want to prevent deleting used contracts? + #my $contract_count = $c->model('DB')->resultset('contracts')->search({ + # contact_id => $id + #}); + #if($contract_count > 0) { + # $self->error($c, HTTP_LOCKED, "Contact is still in use."); + # last; + #} else { + $contract->delete; + #} + $guard->commit; + + $c->response->status(HTTP_NO_CONTENT); + $c->response->body(q()); + } + return; } -sub hal_from_contract :Private { +sub contract_by_id : Private { + my ($self, $c, $id) = @_; + + # we only return system contracts, that is, those with contacts without + # reseller + my $contract_rs = $c->model('DB')->resultset('contracts') + ->search({ + 'contact.reseller_id' => undef + }, { + join => 'contact' + }); + return $contract_rs->find({'me.id' => $id}); +} + +sub hal_from_contract : Private { my ($self, $c, $contract) = @_; - # XXX invalid 00-00-00 dates my %resource = $contract->get_inflated_columns; - my $id = delete $resource{id}; - $self->last_modified(delete $resource{modify_timestamp}); + my $id = $resource{id}; + + # TODO: fixxxxxx + my $billing_profile_id = 1; + my $contract_balance_id = 1; my $hal = Data::HAL->new( links => [ @@ -329,18 +242,18 @@ sub hal_from_contract :Private { name => 'ngcp', templated => true, ), - Data::HAL::Link->new(relation => 'collection', href => '/api/contracts/'), + 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 => "/api/contracts/$id"), - $contract->contact - ? Data::HAL::Link->new( - relation => 'ngcp:contacts', - href => sprintf('/api/contacts/%d', $contract->contact_id), - ) : (), + Data::HAL::Link->new(relation => 'self', href => sprintf("/%s", $c->request->path)), + 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)), ], - relation => 'ngcp:contracts', + relation => 'ngcp:'.$self->resource_name, ); + # TODO: add billing_profile_id, contract_balance_id etc to + # %resource as well + my $form = NGCP::Panel::Form::Contract::PeeringReseller->new; $self->validate_form( c => $c, diff --git a/lib/NGCP/Panel/Role/API.pm b/lib/NGCP/Panel/Role/API.pm index 8fbe78561d..a2d9b362ec 100644 --- a/lib/NGCP/Panel/Role/API.pm +++ b/lib/NGCP/Panel/Role/API.pm @@ -7,7 +7,6 @@ use JSON::Pointer; use HTTP::Status qw(:constants); use Safe::Isa qw($_isa); use Try::Tiny; -use Digest::SHA3 qw(sha3_256_base64); use DateTime::Format::HTTP qw(); use DateTime::Format::RFC3339 qw(); use Types::Standard qw(InstanceOf);