Implement stock consumption via the API [SCI-6642] (#3964)

* Implement stock consumption via the API [SCI-6642]

* Remove unnecessary attribute from InventoryItemSerializer [SCI-6642]

* Amend permission check, add nested transaction support to consume_stock method [SCI-6642]
This commit is contained in:
artoscinote 2022-03-30 14:33:26 +02:00 committed by GitHub
parent ac7a6edab5
commit 229a27750f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 277 additions and 16 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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"

View file

@ -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),

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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