diff --git a/app/controllers/api/v1/task_inventory_items_controller.rb b/app/controllers/api/v1/task_inventory_items_controller.rb index 20d204147..bd2e7f588 100644 --- a/app/controllers/api/v1/task_inventory_items_controller.rb +++ b/app/controllers/api/v1/task_inventory_items_controller.rb @@ -7,6 +7,8 @@ module Api before_action :load_project before_action :load_experiment before_action :load_task + before_action :load_my_module_repository_row, only: :update + before_action :check_stock_consumption_update_permissions, only: :update def index items = @@ -16,20 +18,57 @@ module Api .page(params.dig(:page, :number)) .per(params.dig(:page, :size)) render jsonapi: items, - each_serializer: InventoryItemSerializer, + each_serializer: TaskInventoryItemSerializer, show_repository: true, + my_module: @task, include: include_params end def show render jsonapi: @task.repository_rows.find(params.require(:id)), - serializer: InventoryItemSerializer, + serializer: TaskInventoryItemSerializer, show_repository: true, + my_module: @task, + include: %i(inventory_cells inventory) + end + + def update + @my_module_repository_row.consume_stock( + current_user, + repository_row_params[:attributes][:stock_consumption], + repository_row_params[:attributes][:stock_consumption_comment] + ) + + render jsonapi: @my_module_repository_row.repository_row, + serializer: TaskInventoryItemSerializer, + show_repository: true, + my_module: @task, include: %i(inventory_cells inventory) end private + def load_my_module_repository_row + @my_module_repository_row = @task.repository_rows + .find(params.require(:id)) + .my_module_repository_rows + .find_by(my_module: @task) + end + + def check_stock_consumption_update_permissions + unless can_update_my_module_stock_consumption?(@task) && + can_manage_repository_rows?(@my_module_repository_row.repository_row.repository) + raise PermissionError.new(RepositoryRow, :update_stock_consumption) + end + end + + def repository_row_params + raise TypeError unless params.require(:data).require(:type) == 'inventory_items' + + params.require(:data).require(:attributes) + params.permit(data: { attributes: %i(stock_consumption stock_consumption_comment) })[:data] + end + def permitted_includes %w(inventory_cells) end diff --git a/app/controllers/my_module_repositories_controller.rb b/app/controllers/my_module_repositories_controller.rb index d3792666e..f84022aad 100644 --- a/app/controllers/my_module_repositories_controller.rb +++ b/app/controllers/my_module_repositories_controller.rb @@ -159,20 +159,12 @@ class MyModuleRepositoriesController < ApplicationController def update_consumption module_repository_row = @my_module.my_module_repository_rows.find_by(id: params[:module_row_id]) - module_repository_row.with_lock do - current_stock = module_repository_row.stock_consumption - module_repository_row.assign_attributes( - stock_consumption: params[:stock_consumption], - repository_stock_unit_item_id: - module_repository_row.repository_row.repository_stock_value.repository_stock_unit_item_id, - last_modified_by: current_user, - comment: params[:comment] - ) - module_repository_row.save! - log_activity(module_repository_row, - current_stock, - params[:comment]) + ActiveRecord::Base.transaction do + current_stock = module_repository_row.stock_consumption + module_repository_row.consume_stock(current_user, params[:stock_consumption], params[:comment]) + + log_activity(module_repository_row, current_stock, params[:comment]) end render json: {}, status: :ok diff --git a/app/models/my_module_repository_row.rb b/app/models/my_module_repository_row.rb index f2fe3d001..b5c87762f 100644 --- a/app/models/my_module_repository_row.rb +++ b/app/models/my_module_repository_row.rb @@ -19,6 +19,20 @@ class MyModuleRepositoryRow < ApplicationRecord before_save :nulify_stock_consumption, if: :stock_consumption_changed? + def consume_stock(user, stock_consumption, comment = nil) + ActiveRecord::Base.transaction(requires_new: true) do + lock! + assign_attributes( + stock_consumption: stock_consumption, + repository_stock_unit_item_id: + repository_row.repository_stock_value.repository_stock_unit_item_id, + last_modified_by: user, + comment: comment + ) + save! + end + end + private def nulify_stock_consumption diff --git a/app/permissions/repository.rb b/app/permissions/repository.rb index a463b4f3f..de4dfe671 100644 --- a/app/permissions/repository.rb +++ b/app/permissions/repository.rb @@ -60,6 +60,7 @@ Canaid::Permissions.register_for(Repository) do # repository: create/import record can :create_repository_rows do |user, repository| next false if repository.is_a?(BmtRepository) + next false if repository.archived? if repository.shared_with?(user.current_team) repository.shared_with_write?(user.current_team) && user.is_normal_user_or_admin_of_team?(user.current_team) diff --git a/app/serializers/api/v1/task_inventory_item_serializer.rb b/app/serializers/api/v1/task_inventory_item_serializer.rb new file mode 100644 index 000000000..0a9b16e16 --- /dev/null +++ b/app/serializers/api/v1/task_inventory_item_serializer.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Api + module V1 + class TaskInventoryItemSerializer < InventoryItemSerializer + attribute :stock_consumption, if: -> { object.repository_stock_cell.present? } + + def stock_consumption + object.my_module_repository_rows + .find_by(my_module: instance_options[:my_module]) + .stock_consumption + end + end + end +end diff --git a/config/locales/en.yml b/config/locales/en.yml index 60fd4d327..9940a73ad 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -2964,6 +2964,9 @@ en: read_users_permission: title: "Permission denied" detail: "You don't have permission to read users on %{model}" + update_stock_consumption_permission: + title: "Permission denied" + detail: "You don't have permisson to update stock consumption on this task item." record_not_found: title: "Not found" detail: "%{model} record with id %{id} not found in the specified scope" diff --git a/config/routes.rb b/config/routes.rb index 24808a325..a62f08de7 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -750,7 +750,7 @@ Rails.application.routes.draw do resources :user_assignments, only: %i(index show update), controller: :task_user_assignments - resources :task_inventory_items, only: %i(index show), + resources :task_inventory_items, only: %i(index show update), path: 'items', as: :items resources :task_users, only: %i(index show), diff --git a/spec/factories/repository_cells.rb b/spec/factories/repository_cells.rb index 54200d42a..91dfe7588 100644 --- a/spec/factories/repository_cells.rb +++ b/spec/factories/repository_cells.rb @@ -80,5 +80,12 @@ FactoryBot.define do repository_cell.value ||= build(:repository_checklist_value, repository_cell: repository_cell) end end + + trait :stock_value do + repository_column { create :repository_column, :stock_type, repository: repository_row.repository } + after(:build) do |repository_cell| + repository_cell.value ||= build(:repository_stock_value, repository_cell: repository_cell) + end + end end end diff --git a/spec/factories/repository_columns.rb b/spec/factories/repository_columns.rb index 4fdcc8255..d989f3492 100644 --- a/spec/factories/repository_columns.rb +++ b/spec/factories/repository_columns.rb @@ -50,5 +50,9 @@ FactoryBot.define do trait :checklist_type do data_type { :RepositoryChecklistValue } end + + trait :stock_type do + data_type { :RepositoryStockValue } + end end end diff --git a/spec/factories/repository_stock_unit_item.rb b/spec/factories/repository_stock_unit_item.rb new file mode 100644 index 000000000..47d65f919 --- /dev/null +++ b/spec/factories/repository_stock_unit_item.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :repository_stock_unit_item do + sequence(:data) { |n| "u-#{n}" } + repository_column + created_by { create :user } + last_modified_by { created_by } + end +end diff --git a/spec/factories/repository_stock_values.rb b/spec/factories/repository_stock_values.rb new file mode 100644 index 000000000..1261d654b --- /dev/null +++ b/spec/factories/repository_stock_values.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :repository_stock_value do + created_by { create :user } + last_modified_by { created_by } + repository_stock_unit_item + amount { 1000.0 } + after(:build) do |repository_stock_value| + repository_stock_value.repository_cell ||= build(:repository_cell, + :stock_value, + repository_stock_value: repository_stock_value) + end + end +end diff --git a/spec/requests/api/v1/task_inventory_items_controller_spec.rb b/spec/requests/api/v1/task_inventory_items_controller_spec.rb new file mode 100644 index 000000000..018e0f878 --- /dev/null +++ b/spec/requests/api/v1/task_inventory_items_controller_spec.rb @@ -0,0 +1,161 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Api::V1::TasksController', type: :request do + before :all do + ApplicationSettings.instance.update( + values: ApplicationSettings.instance.values.merge({"stock_management_enabled" => true}) + ) + MyModuleStatusFlow.ensure_default + + @user = create(:user) + @team = create(:team, created_by: @user) + create(:user_team, user: @user, team: @team, role: 1) + @owner_role = UserRole.find_by(name: I18n.t('user_roles.predefined.owner')) + @project = create(:project, name: Faker::Name.unique.name, + created_by: @user, team: @team) + + @experiment = create(:experiment, created_by: @user, + last_modified_by: @user, project: @project) + + @my_module = create( + :my_module, + :with_due_date, + created_by: @user, + last_modified_by: @user, + experiment: @experiment + ) + + @repository = create(:repository) + create(:user_team, user: @user, team: @repository.team, role: 1) + #@repository_stock_column = create(:repository_column, :stock_type, repository: @repository) + @repository_row = create(:repository_row, name: 'Test row', repository: @repository) + @repository_stock_cell = create( + :repository_cell, + :stock_value, + repository_row: @repository_row + ) + + @my_module_repository_row = create( + :mm_repository_row, + my_module: @my_module, + repository_row: @repository_row, + assigned_by: @user, + last_modified_by: @user, + stock_consumption: 5.0 + ) + + @valid_headers = + { 'Authorization': 'Bearer ' + generate_token(@user.id) } + end + + describe 'GET task_inventory_items, #index' do + it 'Response with correct task inventory items' do + get( + api_v1_team_project_experiment_task_items_url( + team_id: @team.id, + project_id: @project.id, + experiment_id: @experiment.id, + task_id: @my_module.id + ), + headers: @valid_headers + ) + + expect(response).to have_http_status 200 + expect(JSON.parse(response.body)['data'].map { |item| item['id'] }).to( + eq([@my_module_repository_row.id.to_s]) + ) + end + end + + describe 'GET task inventory items, #show' do + it 'When valid request, user can read task inventory item' do + get( + api_v1_team_project_experiment_task_item_url( + team_id: @team.id, + project_id: @project.id, + experiment_id: @experiment.id, + task_id: @my_module.id, + id: @my_module_repository_row.id + ), + headers: @valid_headers + ) + + expect(response).to have_http_status 200 + expect(JSON.parse(response.body)['data']['id']).to( + eq(@my_module_repository_row.id.to_s) + ) + end + end + + describe 'PATCH task inventory items, #update' do + before :all do + @valid_headers['Content-Type'] = 'application/json' + end + + let(:request_body) do + { + data: { + type: 'inventory_items', + attributes: { + stock_consumption: 100.0, + stock_consumption_comment: 'Some comment.' + } + } + } + end + + context 'when has valid params' do + let(:action) do + patch(api_v1_team_project_experiment_task_item_url( + team_id: @team.id, + project_id: @project.id, + experiment_id: @experiment.id, + task_id: @my_module.id, + id: @my_module_repository_row.id + ), + params: request_body.to_json, + headers: @valid_headers) + end + + it 'updates stock consumption' do + action + expect(@my_module_repository_row.reload.stock_consumption).to eq(100.0) + end + + it 'returns well formated response' do + action + + expect(json).to match( + hash_including( + data: hash_including( + type: 'inventory_items', + attributes: hash_including(stock_consumption: "100.0") ) + ) + ) + end + end + + context 'when has not valid params' do + let(:action) do + patch(api_v1_team_project_experiment_task_item_url( + team_id: @team.id, + project_id: @project.id, + experiment_id: @experiment.id, + task_id: @my_module.id, + id: @my_module_repository_row.id + ), + params: request_body.to_json, + headers: @valid_headers) + end + + it 'renders 403 when repository row is locked' do + @repository.update(archived: true) + action + + expect(response).to have_http_status(403) + end + end + end +end