From 9e3653341aa1dcfda9d29ce5ccddfad1fb4f45d0 Mon Sep 17 00:00:00 2001
From: Kirill Solomko <ksolomko@sipwise.com>
Date: Thu, 4 Apr 2024 16:18:49 +0200
Subject: [PATCH] MT#59478 adapt /api/cftimesets to global transactions, more
 fixes

* /api/cftimesets now fully use Entities/EntitiesItem
* EntitiesItem: delete(): fix delete_item() expression processing
* Entities/EntitiesItem: post/put/patch/delete: go to TX_END
  in scenarios where after a method call (e.g. update_item()) there
  is a normal return from the function but errors in $c->error, so
  that they are also caught correctly

Change-Id: I3bef409ded590796c2bba4f30acd28b02e99065b
---
 lib/NGCP/Panel/Controller/API/CFTimeSets.pm   | 156 +++++-------------
 .../Panel/Controller/API/CFTimeSetsItem.pm    | 125 ++------------
 lib/NGCP/Panel/Role/API/CFTimeSets.pm         |  21 ++-
 lib/NGCP/Panel/Role/EntitiesItem.pm           |  14 +-
 4 files changed, 89 insertions(+), 227 deletions(-)

diff --git a/lib/NGCP/Panel/Controller/API/CFTimeSets.pm b/lib/NGCP/Panel/Controller/API/CFTimeSets.pm
index cfcc121ef5..142c781cba 100644
--- a/lib/NGCP/Panel/Controller/API/CFTimeSets.pm
+++ b/lib/NGCP/Panel/Controller/API/CFTimeSets.pm
@@ -61,124 +61,56 @@ __PACKAGE__->set_config({
     allowed_roles => [qw/admin reseller ccareadmin ccare subscriberadmin subscriber/],
 });
 
-sub GET :Allow {
-    my ($self, $c) = @_;
-    my $page = $c->request->params->{page} // 1;
-    my $rows = $c->request->params->{rows} // 10;
-    {
-        my $timesets = $self->item_rs($c);
-
-        (my $total_count, $timesets, my $timesets_rows) = $self->paginate_order_collection($c, $timesets);
-        my (@embedded, @links);
-        for my $tset (@$timesets_rows) {
-            push @embedded, $self->hal_from_item($c, $tset, "cftimesets");
-            push @links, Data::HAL::Link->new(
-                relation => 'ngcp:'.$self->resource_name,
-                href     => sprintf('%s%d', $self->dispatch_path, $tset->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/'),
-            $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,
+sub create_item {
+    my ($self, $c, $resource, $form, $process_extras) = @_;
+
+    my $schema = $c->model('DB');
+
+    my $tset;
+
+    if($c->user->roles eq "subscriberadmin" || $c->user->roles eq "subscriber") {
+        $resource->{subscriber_id} = $c->user->voip_subscriber->id;
+    } elsif(!defined $resource->{subscriber_id}) {
+        $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Missing mandatory field 'subscriber_id'");
+        return;
+    }
+    my $b_subscriber = $schema->resultset('voip_subscribers')->find({
+            id => $resource->{subscriber_id},
         });
-        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);
+    unless($b_subscriber) {
+        $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Invalid 'subscriber_id'.");
         return;
     }
-    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 $form = $self->get_form($c);
-        last unless $self->validate_form(
-            c => $c,
-            resource => $resource,
-            form => $form,
-        );
-
-        my $tset;
-
-        if($c->user->roles eq "subscriberadmin" || $c->user->roles eq "subscriber") {
-            $resource->{subscriber_id} = $c->user->voip_subscriber->id;
-        } elsif(!defined $resource->{subscriber_id}) {
-            $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Missing mandatory field 'subscriber_id'");
-            last;
-        }
-        my $b_subscriber = $schema->resultset('voip_subscribers')->find({
-                id => $resource->{subscriber_id},
+    my $subscriber = $b_subscriber->provisioning_voip_subscriber;
+    unless($subscriber) {
+        $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Invalid subscriber.");
+        return;
+    }
+    if (! exists $resource->{times} ) {
+        $resource->{times} = [];
+    }
+    if (ref $resource->{times} ne "ARRAY") {
+        $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Invalid field 'times'. Must be an array.");
+        return;
+    }
+    my $times = $resource->{times};
+    # enable tz and use_owner_tz params for POST:
+    #$times = $self->apply_owner_timezone($c,$b_subscriber,$resource->{times},'deflate');
+    try {
+        $tset = $schema->resultset('voip_cf_time_sets')->create({
+                name => $resource->{name},
+                subscriber_id => $subscriber->id,
             });
-        unless($b_subscriber) {
-            $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Invalid 'subscriber_id'.");
-            last;
-        }
-        my $subscriber = $b_subscriber->provisioning_voip_subscriber;
-        unless($subscriber) {
-            $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Invalid subscriber.");
-            last;
-        }
-        if (! exists $resource->{times} ) {
-            $resource->{times} = [];
-        }
-        if (ref $resource->{times} ne "ARRAY") {
-            $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Invalid field 'times'. Must be an array.");
-            last;
-        }
-        my $times = $resource->{times};
-        # enable tz and use_owner_tz params for POST:
-        #$times = $self->apply_owner_timezone($c,$b_subscriber,$resource->{times},'deflate');
-        try {
-            $tset = $schema->resultset('voip_cf_time_sets')->create({
-                    name => $resource->{name},
-                    subscriber_id => $subscriber->id,
-                });
-            for my $t ( @$times ) {
-                delete $t->{time_set_id};
-                $tset->create_related("voip_cf_periods", $t);
-            }
-        } catch($e) {
-            $self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Failed to create cftimeset.", $e);
-            last;
+        for my $t ( @$times ) {
+            delete $t->{time_set_id};
+            $tset->create_related("voip_cf_periods", $t);
         }
-
-        last unless $self->add_create_journal_item_hal($c,sub {
-            my $self = shift;
-            my ($c) = @_;
-            my $_dset = $self->item_by_id($c, $tset->id);
-            return $self->hal_from_item($c, $_dset, "cftimesets"); });
-
-        $guard->commit;
-
-        $c->response->status(HTTP_CREATED);
-        $c->response->header(Location => sprintf('/%s%d', $c->request->path, $tset->id));
-        $c->response->body(q());
+    } catch($e) {
+        $self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Failed to create cftimeset.", $e);
+        return;
     }
-    return;
+
+    return $tset;
 }
 
 1;
diff --git a/lib/NGCP/Panel/Controller/API/CFTimeSetsItem.pm b/lib/NGCP/Panel/Controller/API/CFTimeSetsItem.pm
index 22983bda36..313f095809 100644
--- a/lib/NGCP/Panel/Controller/API/CFTimeSetsItem.pm
+++ b/lib/NGCP/Panel/Controller/API/CFTimeSetsItem.pm
@@ -41,124 +41,25 @@ __PACKAGE__->set_config({
     allowed_roles => {
         Default => [qw/admin reseller ccareadmin ccare subscriberadmin subscriber/],
         Journal => [qw/admin reseller ccareadmin ccare/],
-    }
+    },
+    PATCH => { ops => [qw/add replace remove copy/] },
 });
 
-sub GET :Allow {
-    my ($self, $c, $id) = @_;
-    {
-        last unless $self->valid_id($c, $id);
-        my $tset = $self->item_by_id($c, $id);
-        last unless $self->resource_exists($c, timeset => $tset);
-
-        my $hal = $self->hal_from_item($c, $tset, "cftimesets");
-
-        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 delete_item {
+    my ($self, $c, $item) = @_;
 
-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',
-            ops => [qw/add replace remove copy/],
-        );
-        last unless $json;
-
-        my $tset = $self->item_by_id($c, $id);
-        last unless $self->resource_exists($c, timeset => $tset);
-        my $old_resource = $self->hal_from_item($c, $tset, "cftimesets")->resource;
-        my $resource = $self->apply_patch($c, $old_resource, $json);
-        last unless $resource;
-
-        my $form = $self->get_form($c);
-        $tset = $self->update_item($c, $tset, $old_resource, $resource, $form);
-        last unless $tset;
-
-        my $hal = $self->hal_from_item($c, $tset, "cftimesets");
-        last unless $self->add_update_journal_item_hal($c,$hal);
-        
-        $guard->commit;
-
-        $self->return_representation($c, 'hal' => $hal, 'preference' => $preference );
-    }
-    return;
-}
+    return unless $self->check_subscriber_can_update_item($c, $item);
 
-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 $tset = $self->item_by_id($c, $id);
-        last unless $self->resource_exists($c, timeset => $tset);
-        my $resource = $self->get_valid_put_data(
-            c => $c,
-            id => $id,
-            media_type => 'application/json',
-        );
-        last unless $resource;
-        my $old_resource = undef; #unused: { $tset->get_inflated_columns };
-
-        my $form = $self->get_form($c);
-        $tset = $self->update_item($c, $tset, $old_resource, $resource, $form);
-        last unless $tset;
-        
-        my $hal = $self->hal_from_item($c, $tset, "cftimesets");
-        last unless $self->add_update_journal_item_hal($c,$hal);
-
-        $guard->commit;
-
-        $self->return_representation($c, 'hal' => $hal, 'preference' => $preference );
+    try {
+        $item->delete;
+    } catch($e) {
+        my $id = $item->id;
+        $self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Internal Server Error",
+                     "Failed to delete cftimeset with id '$id'", $e);
+        return;
     }
-    return;
-}
 
-sub DELETE :Allow {
-    my ($self, $c, $id) = @_;
-    my $guard = $c->model('DB')->txn_scope_guard;
-    {
-        my $tset = $self->item_by_id($c, $id);
-        last unless $self->resource_exists($c, timeset => $tset);
-
-        return unless $self->check_subscriber_can_update_item($c, $tset);
-        
-        last unless $self->add_delete_journal_item_hal($c,sub {
-            my $self = shift;
-            my ($c) = @_;
-            return $self->hal_from_item($c, $tset, "cftimesets"); });
-        
-        try {
-            $tset->delete;
-        } catch($e) {
-            $self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Internal Server Error",
-                         "Failed to delete cftimeset with id '$id'", $e);
-            last;
-        }
-        $guard->commit;
-
-        $c->response->status(HTTP_NO_CONTENT);
-        $c->response->body(q());
-    }
-    return;
+    return 1;
 }
 
 sub get_journal_methods{
diff --git a/lib/NGCP/Panel/Role/API/CFTimeSets.pm b/lib/NGCP/Panel/Role/API/CFTimeSets.pm
index 08009451e2..2bedac5fe6 100644
--- a/lib/NGCP/Panel/Role/API/CFTimeSets.pm
+++ b/lib/NGCP/Panel/Role/API/CFTimeSets.pm
@@ -300,8 +300,9 @@ sub _merge_adjacent {
 }
 
 sub hal_from_item {
-    my ($self, $c, $item, $type) = @_;
-    my $form;
+    my ($self, $c, $item, $form, $params) = @_;
+
+    my $type = 'cftimesets';
 
     my %resource = $item->get_inflated_columns;
     my @times;
@@ -400,6 +401,22 @@ sub check_subscriber_can_update_item {
     return 1;
 }
 
+sub resource_from_item {
+    my ($self, $c, $item) = @_;
+
+    my $resource = { $item->get_inflated_columns };
+
+    $resource->{subscriber_id} = $item->subscriber->voip_subscriber->id;
+    $resource->{times} //= [];
+
+    foreach my $period ($item->voip_cf_periods->all) {
+        push @{$resource->{times}}, { $period->get_inflated_columns };
+    }
+
+    return $resource;
+
+}
+
 sub update_item {
     my ($self, $c, $item, $old_resource, $resource, $form) = @_;
 
diff --git a/lib/NGCP/Panel/Role/EntitiesItem.pm b/lib/NGCP/Panel/Role/EntitiesItem.pm
index d1cb2a9413..900865aac9 100644
--- a/lib/NGCP/Panel/Role/EntitiesItem.pm
+++ b/lib/NGCP/Panel/Role/EntitiesItem.pm
@@ -294,6 +294,9 @@ sub patch {
             #$old_resource = clone($old_resource);
             ##without it error: The entity could not be processed: Modification of a read-only value attempted at /usr/share/perl5/JSON/Pointer.pm line 200, <$fh> line 1.\n
             $resource = $self->apply_patch($c, $old_resource, $json);
+            if ($c->has_errors) {
+                goto TX_END;
+            }
             unless ($resource) {
                 $self->error($c, HTTP_INTERNAL_SERVER_ERROR, 'Internal Server Error',
                              '#apply_patch') unless $c->has_errors;
@@ -301,6 +304,9 @@ sub patch {
             }
 
             ($item, $form, $process_extras) = $self->update_item($c, $item, $old_resource, $resource, $form, $process_extras );
+            if ($c->has_errors) {
+                goto TX_END;
+            }
             unless ($item) {
                 $self->error($c, HTTP_INTERNAL_SERVER_ERROR, 'Internal Server Error',
                              '#update_item') unless $c->has_errors;
@@ -392,6 +398,9 @@ sub put {
             my ($data_processed_result);
             if (!$non_json_data || !$data) {
                 ($item, $form, $process_extras) = $self->update_item($c, $item, $old_resource, $resource, $form, $process_extras );
+                if ($c->has_errors) {
+                    goto TX_END;
+                }
                 unless ($item) {
                     $self->error($c, HTTP_INTERNAL_SERVER_ERROR, 'Internal Server Error',
                                  '#update_item') unless $c->has_errors;
@@ -469,13 +478,16 @@ sub delete {  ## no critic (ProhibitBuiltinHomonyms)
             }
 
             ($hal, $hal_id) = $self->get_journal_item_hal($c, $item);
-            unless ($self->delete_item($c, $item)) {
+            if ($self->delete_item($c, $item)) {
                 unless ($self->add_journal_item_hal($c, { hal => $hal, ($hal_id ? ( id => $hal_id, ) : ()) })) {
                     $self->error($c, HTTP_INTERNAL_SERVER_ERROR, 'Internal Server Error',
                                  '#add_journal_item_hal');
                     goto TX_END;
                 }
             } else {
+                if ($c->has_errors) {
+                    goto TX_END;
+                }
                 $self->error($c, HTTP_INTERNAL_SERVER_ERROR, 'Internal Server Error',
                              'non 1 return from $self->delete_item()');
                 goto TX_END;