diff --git a/app/controllers/api/v1/inventory_stock_unit_items_controller.rb b/app/controllers/api/v1/inventory_stock_unit_items_controller.rb new file mode 100644 index 000000000..3fd66692e --- /dev/null +++ b/app/controllers/api/v1/inventory_stock_unit_items_controller.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +module Api + module V1 + class InventoryStockUnitItemsController < BaseController + before_action :load_team + before_action :load_inventory + before_action :load_inventory_column + before_action :check_column_type + before_action :load_inventory_stock_unit_item, only: %i(show update destroy) + before_action :check_manage_permissions, only: %i(create update destroy) + + def index + stock_unit_items = @inventory_column.repository_stock_unit_items + .page(params.dig(:page, :number)) + .per(params.dig(:page, :size)) + render jsonapi: stock_unit_items, each_serializer: InventoryStockUnitItemSerializer + end + + def create + stock_unit_item = @inventory_column.repository_stock_unit_items + .create!(inventory_stock_unit_item_params) + render jsonapi: stock_unit_item, + serializer: InventoryStockUnitItemSerializer, + status: :created + end + + def show + render jsonapi: @inventory_stock_unit_item, + serializer: InventoryStockUnitItemSerializer + end + + def update + @inventory_stock_unit_item.attributes = update_inventory_stock_unit_item_params + if @inventory_stock_unit_item.changed? && @inventory_stock_unit_item.save! + render jsonapi: @inventory_stock_unit_item, + serializer: InventoryStockUnitItemSerializer + else + render body: nil, status: :no_content + end + end + + def destroy + @inventory_stock_unit_item.destroy! + render body: nil + end + + private + + def check_column_type + raise TypeError unless @inventory_column.data_type == 'RepositoryStockValue' + end + + def load_inventory_stock_unit_item + @inventory_stock_unit_item = + @inventory_column.repository_stock_unit_items.find(params.require(:id)) + end + + def check_manage_permissions + raise PermissionError.new(RepositoryStockUnitItem, :manage) + unless can_manage_repository_column?(@inventory_column) + end + + def inventory_stock_unit_item_params + raise TypeError unless params.require(:data).require(:type) == 'inventory_stock_unit_items' + + params.require(:data).require(:attributes) + params.permit(data: { attributes: %i(data) })[:data].merge( + created_by: @current_user, + last_modified_by: @current_user + ) + end + + def update_inventory_stock_unit_item_params + raise IDMismatchError unless params.require(:data).require(:id).to_i == params[:id].to_i + + inventory_stock_unit_item_params[:attributes] + end + end + end +end diff --git a/app/models/repository_column.rb b/app/models/repository_column.rb index 19aae9925..742e731af 100644 --- a/app/models/repository_column.rb +++ b/app/models/repository_column.rb @@ -38,6 +38,8 @@ class RepositoryColumn < ApplicationRecord after_create :update_repository_table_states_with_new_column after_update :clear_hidden_repository_cell_reminders + + before_destroy :prevent_stock_column_destroy around_destroy :update_repository_table_states_with_removed_column scope :list_type, -> { where(data_type: 'RepositoryListValue') } @@ -169,4 +171,8 @@ class RepositoryColumn < ApplicationRecord .where(repository_columns: { id: id }) .delete_all end + + def prevent_stock_column_destroy + raise NotImplementedError unless deletable? + end end diff --git a/app/serializers/api/v1/inventory_column_serializer.rb b/app/serializers/api/v1/inventory_column_serializer.rb index 1f99332b3..cedf7546d 100644 --- a/app/serializers/api/v1/inventory_column_serializer.rb +++ b/app/serializers/api/v1/inventory_column_serializer.rb @@ -29,6 +29,14 @@ module Api object.data_type == 'RepositoryStatusValue' && !instance_options[:hide_list_items] end) + has_many :repository_stock_unit_items, + key: :repository_stock_unit_items, + serializer: InventoryStockUnitItemSerializer, + class_name: 'RepositoryStockUnitItem', + if: (lambda do + object.data_type == 'RepositoryStockValue' && + !instance_options[:hide_list_items] + end) include TimestampableModel diff --git a/app/serializers/api/v1/inventory_stock_unit_item_serializer.rb b/app/serializers/api/v1/inventory_stock_unit_item_serializer.rb new file mode 100644 index 000000000..495910385 --- /dev/null +++ b/app/serializers/api/v1/inventory_stock_unit_item_serializer.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module Api + module V1 + class InventoryStockUnitItemSerializer < ActiveModel::Serializer + type :inventory_stock_unit_items + attributes :id, :data + + include TimestampableModel + end + end +end diff --git a/config/initializers/extends.rb b/config/initializers/extends.rb index 3994641ff..bdf5db741 100644 --- a/config/initializers/extends.rb +++ b/config/initializers/extends.rb @@ -164,7 +164,8 @@ class Extends 'RepositoryListValue' => 'list', 'RepositoryChecklistValue' => 'checklist', 'RepositoryAssetValue' => 'file', - 'RepositoryStatusValue' => 'status' } + 'RepositoryStatusValue' => 'status', + 'RepositoryStockValue' => 'stock_value' } OMNIAUTH_PROVIDERS = %i(linkedin customazureactivedirectory okta) diff --git a/config/routes.rb b/config/routes.rb index a62f08de7..856548bc9 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -720,6 +720,10 @@ Rails.application.routes.draw do only: %i(index create show update destroy), path: 'status_items', as: :status_items + resources :inventory_stock_unit_items, + only: %i(index create show update destroy), + path: 'stock_unit_items', + as: :stock_unit_items end resources :inventory_items, only: %i(index create show update destroy), diff --git a/spec/requests/api/v1/inventory_columns_controller_spec.rb b/spec/requests/api/v1/inventory_columns_controller_spec.rb index 18dda5415..80819a9da 100644 --- a/spec/requests/api/v1/inventory_columns_controller_spec.rb +++ b/spec/requests/api/v1/inventory_columns_controller_spec.rb @@ -4,6 +4,10 @@ require 'rails_helper' RSpec.describe 'Api::V1::InventoryColumnsController', type: :request do before :all do + ApplicationSettings.instance.update( + values: ApplicationSettings.instance.values.merge({"stock_management_enabled" => true}) + ) + @user = create(:user) @teams = create_list(:team, 2, created_by: @user) create(:user_team, user: @user, team: @teams.first, role: 2) @@ -11,11 +15,16 @@ RSpec.describe 'Api::V1::InventoryColumnsController', type: :request do # valid_inventory @valid_inventory = create(:repository, name: Faker::Name.unique.name, created_by: @user, team: @teams.first) + # valid_stock inventory + @valid_stock_inventory = create(:repository, name: Faker::Name.unique.name, + created_by: @user, team: @teams.first) # unaccessable_inventory create(:repository, name: Faker::Name.unique.name, created_by: @user, team: @teams.second) - + + stock_column = create(:repository_column, name: Faker::Name.unique.name, + data_type: :RepositoryStockValue, repository: @valid_stock_inventory) create(:repository_column, name: Faker::Name.unique.name, repository: @valid_inventory, data_type: :RepositoryTextValue) list_column = create(:repository_column, name: Faker::Name.unique.name, @@ -222,6 +231,16 @@ RSpec.describe 'Api::V1::InventoryColumnsController', type: :request do } } } end + let!(:request_body_stock) {{ data: + { type: 'inventory_columns', + attributes: { + name: Faker::Name.unique.name, + data_type: 'stock_value', + metadata: { + decimals: 3 + } + } } }} + it 'Response with correct inventory column' do hash_body = nil post api_v1_team_inventory_columns_path( @@ -336,6 +355,32 @@ RSpec.describe 'Api::V1::InventoryColumnsController', type: :request do expect(hash_body['errors'][0]).to include('status': 400) end end + + it 'Response with correct stock inventory column' do + hash_body = nil + post api_v1_team_inventory_columns_path( + team_id: @teams.first.id, + inventory_id: @teams.first.repositories.first.id + ), params: request_body_stock.to_json, headers: @valid_headers + expect(response).to have_http_status 201 + expect { hash_body = json }.not_to raise_exception + expect(hash_body[:data]).to match( + JSON.parse( + ActiveModelSerializers::SerializableResource + .new(RepositoryColumn.last, serializer: Api::V1::InventoryColumnSerializer, hide_list_items: true) + .to_json + )['data'] + ) + end + + it 'Raised error with already exsisting stock column in column' do + hash_body = nil + post api_v1_team_inventory_columns_path( + team_id: @teams.first.id, + inventory_id: @teams.first.repositories.second.id + ), params: request_body_stock.to_json, headers: @valid_headers + expect(response).to have_http_status 400 + end end describe 'DELETE inventory_columns, #destroy' do @@ -381,6 +426,17 @@ RSpec.describe 'Api::V1::InventoryColumnsController', type: :request do expect(response).to have_http_status(403) expect(RepositoryColumn.where(id: deleted_id)).to exist end + + it 'Destroy Stock inventory column' do + deleted_id = @teams.first.repositories.second.repository_columns.last.id + delete api_v1_team_inventory_column_path( + id: deleted_id, + team_id: @teams.first.id, + inventory_id: @teams.first.repositories.second.id + ), headers: @valid_headers + expect(response).to have_http_status(400) + expect(RepositoryColumn.where(id: deleted_id)).to exist + end end describe 'PATCH inventory_column, #update' do @@ -392,6 +448,14 @@ RSpec.describe 'Api::V1::InventoryColumnsController', type: :request do ) end + let!(:request_body_stock_update) { + ActiveModelSerializers::SerializableResource.new( + @teams.first.repositories.second.repository_columns.last, + serializer: Api::V1::InventoryColumnSerializer + ) + + } + it 'Response with correctly updated inventory column' do hash_body = nil updated_inventory_column = @inventory_column.as_json @@ -410,6 +474,23 @@ RSpec.describe 'Api::V1::InventoryColumnsController', type: :request do expect(hash_body['data']['attributes']['name']).to match(returned_inventory_column[:data][:attributes][:name]) end + it 'Response with correctly updated inventory stock column' do + hash_body = nil + updated_inventory_column = request_body_stock_update.as_json + updated_inventory_column[:data][:attributes][:metadata][:decimals] = 0 + returned_inventory_column = updated_inventory_column.deep_dup + updated_inventory_column[:data][:attributes].delete(:data_type) + patch api_v1_team_inventory_column_path( + id: @teams.first.repositories.second.repository_columns.last.id, + team_id: @teams.first.id, + inventory_id: @teams.first.repositories.second.id + ), params: updated_inventory_column.to_json, + headers: @valid_headers + expect(response).to have_http_status 200 + expect { hash_body = json }.not_to raise_exception + expect(hash_body['data']['attributes']['metadata']['decimals']).to match(returned_inventory_column[:data][:attributes][:metadata][:decimals]) + end + it 'Invalid request, wrong team' do hash_body = nil updated_inventory_column = @inventory_column.as_json diff --git a/spec/requests/api/v1/inventory_stock_unit_item_controller_spec.rb b/spec/requests/api/v1/inventory_stock_unit_item_controller_spec.rb new file mode 100644 index 000000000..179de7f52 --- /dev/null +++ b/spec/requests/api/v1/inventory_stock_unit_item_controller_spec.rb @@ -0,0 +1,381 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Api::V1::InventoryStockUnitItemsController', type: :request do + before :all do + @user = create(:user) + @teams = create_list(:team, 2, created_by: @user) + create(:user_team, user: @user, team: @teams.first, role: 2) + + @valid_inventory = create(:repository, name: Faker::Name.unique.name, + created_by: @user, team: @teams.first) + + @wrong_inventory = create(:repository, name: Faker::Name.unique.name, + created_by: @user, team: @teams.second) + create(:repository_column, name: Faker::Name.unique.name, + repository: @wrong_inventory, data_type: :RepositoryTextValue) + + @text_column = create(:repository_column, name: Faker::Name.unique.name, + repository: @valid_inventory, data_type: :RepositoryTextValue) + @stock_column = create(:repository_column, name: Faker::Name.unique.name, + repository: @valid_inventory, data_type: :RepositoryStockValue) + @wrong_stock_unit_column = create(:repository_column, + name: Faker::Name.unique.name, + repository: @wrong_inventory, + data_type: :RepositoryStockValue) + create_list(:repository_stock_unit_item, 10, repository_column: @stock_column) + create(:repository_stock_unit_item, repository_column: @wrong_stock_unit_column) + + @valid_headers = { 'Authorization': 'Bearer ' + generate_token(@user.id) } + end + + describe 'GET inventory_stock_unit_items, #index' do + it 'Response with correct inventory stock unit items, default per page' do + hash_body = nil + get api_v1_team_inventory_column_stock_unit_items_path( + team_id: @teams.first.id, + inventory_id: @teams.first.repositories.first.id, + column_id: @stock_column.id + ), headers: @valid_headers + expect { hash_body = json }.not_to raise_exception + expect(hash_body[:data]).to match( + JSON.parse( + ActiveModelSerializers::SerializableResource + .new(@stock_column.repository_stock_unit_items.limit(10), + each_serializer: Api::V1::InventoryStockUnitItemSerializer) + .to_json + )['data'] + ) + end + + it 'When invalid request, user in not member of the team' do + hash_body = nil + get api_v1_team_inventory_column_stock_unit_items_path( + team_id: @wrong_inventory.team.id, + inventory_id: @wrong_inventory.id, + column_id: @wrong_inventory.repository_columns.first.id + ), headers: @valid_headers + expect(response).to have_http_status(403) + expect { hash_body = json }.not_to raise_exception + expect(hash_body['errors'][0]).to include('status': 403) + end + + it 'When invalid request, non existing inventory' do + hash_body = nil + get api_v1_team_inventory_column_stock_unit_items_path( + team_id: @teams.first.id, + inventory_id: 123, + column_id: 999 + ), headers: @valid_headers + expect(response).to have_http_status(404) + expect { hash_body = json }.not_to raise_exception + expect(hash_body['errors'][0]).to include('status': 404) + end + + it 'When invalid request, repository from another team' do + hash_body = nil + get api_v1_team_inventory_column_stock_unit_items_path( + team_id: @teams.first.id, + inventory_id: @wrong_inventory.id, + column_id: @stock_column.id + ), headers: @valid_headers + expect(response).to have_http_status(404) + expect { hash_body = json }.not_to raise_exception + expect(hash_body['errors'][0]).to include('status': 404) + end + + it 'When invalid request, items from text column' do + hash_body = nil + get api_v1_team_inventory_column_stock_unit_items_path( + team_id: @teams.first.id, + inventory_id: @valid_inventory.id, + column_id: @text_column.id + ), headers: @valid_headers + expect(response).to have_http_status(400) + expect { hash_body = json }.not_to raise_exception + expect(hash_body['errors'][0]).to include('status': 400) + end + end + + describe 'GET inventory_stock_unit_items, #show' do + it 'Response with correct inventory stock unit item' do + hash_body = nil + get api_v1_team_inventory_column_stock_unit_item_path( + id: @stock_column.repository_stock_unit_items.first.id, + team_id: @teams.first.id, + inventory_id: @teams.first.repositories.first.id, + column_id: @stock_column.id + ), headers: @valid_headers + expect { hash_body = json }.not_to raise_exception + expect(hash_body[:data]).to match( + JSON.parse( + ActiveModelSerializers::SerializableResource + .new(@stock_column.repository_stock_unit_items.first, serializer: Api::V1::InventoryStockUnitItemSerializer) + .to_json + )['data'] + ) + end + + it 'When invalid request, non existing stock unit item' do + hash_body = nil + get api_v1_team_inventory_column_stock_unit_item_path( + id: 999, + team_id: @teams.first.id, + inventory_id: @teams.first.repositories.first.id, + column_id: @stock_column.id + ), headers: @valid_headers + expect(response).to have_http_status(404) + expect { hash_body = json }.not_to raise_exception + expect(hash_body['errors'][0]).to include('status': 404) + end + + it 'When invalid request, stock unit item from another column' do + hash_body = nil + get api_v1_team_inventory_column_stock_unit_items_path( + id: @wrong_stock_unit_column.repository_stock_unit_items.first.id, + team_id: @teams.first.id, + inventory_id: @wrong_inventory.id, + column_id: @stock_column.id + ), headers: @valid_headers + expect(response).to have_http_status(404) + expect { hash_body = json }.not_to raise_exception + expect(hash_body['errors'][0]).to include('status': 404) + end + end + + describe 'POST inventory_stock_unit_item, #create' do + before :all do + @valid_headers['Content-Type'] = 'application/vnd.api+json' + @request_body = { + data: { + type: 'inventory_stock_unit_items', + attributes: { data: Faker::Name.unique.name } + } + } + end + + it 'Response with correct inventory stock unit item' do + hash_body = nil + post api_v1_team_inventory_column_stock_unit_items_path( + team_id: @teams.first.id, + inventory_id: @valid_inventory.id, + column_id: @stock_column + ), params: @request_body.to_json, headers: @valid_headers + expect(response).to have_http_status 201 + expect { hash_body = json }.not_to raise_exception + expect(hash_body[:data]).to match( + JSON.parse( + ActiveModelSerializers::SerializableResource + .new(RepositoryStockUnitItem.last, serializer: Api::V1::InventoryStockUnitItemSerializer) + .to_json + )['data'] + ) + end + + it 'When invalid request, user in not member of the team' do + hash_body = nil + post api_v1_team_inventory_column_stock_unit_items_path( + team_id: @teams.second.id, + inventory_id: @valid_inventory.id, + column_id: @stock_column + ), params: @request_body.to_json, headers: @valid_headers + expect(response).to have_http_status 403 + expect { hash_body = json }.not_to raise_exception + expect(hash_body['errors'][0]).to include('status': 403) + end + + it 'When invalid request, non existing inventory' do + hash_body = nil + post api_v1_team_inventory_column_stock_unit_items_path( + team_id: @teams.first.id, + inventory_id: 123, + column_id: @stock_column + ), params: @request_body.to_json, headers: @valid_headers + expect(response).to have_http_status 404 + expect { hash_body = json }.not_to raise_exception + expect(hash_body['errors'][0]).to include('status': 404) + end + + it 'When invalid request, repository from another team' do + hash_body = nil + post api_v1_team_inventory_column_stock_unit_items_path( + team_id: @teams.first.id, + inventory_id: @wrong_inventory.id, + column_id: @stock_column + ), params: @request_body.to_json, headers: @valid_headers + expect(response).to have_http_status(404) + expect { hash_body = json }.not_to raise_exception + expect(hash_body['errors'][0]).to include('status': 404) + end + + it 'When invalid request, incorrect type' do + invalid_request_body = @request_body.deep_dup + invalid_request_body[:data][:type] = 'repository_rows' + hash_body = nil + post api_v1_team_inventory_column_stock_unit_items_path( + team_id: @teams.first.id, + inventory_id: @valid_inventory.id, + column_id: @stock_column + ), params: invalid_request_body.to_json, headers: @valid_headers + expect(response).to have_http_status(400) + expect { hash_body = json }.to_not raise_exception + expect(hash_body['errors'][0]).to include('status': 400) + end + + it 'When invalid request, missing type param' do + invalid_request_body = @request_body.deep_dup + invalid_request_body[:data].delete(:type) + hash_body = nil + post api_v1_team_inventory_column_stock_unit_items_path( + team_id: @teams.first.id, + inventory_id: @valid_inventory.id, + column_id: @stock_column + ), params: invalid_request_body.to_json, headers: @valid_headers + expect(response).to have_http_status(400) + expect { hash_body = json }.to_not raise_exception + expect(hash_body['errors'][0]).to include('status': 400) + end + + it 'When invalid request, missing attributes values' do + hash_body = nil + invalid_request_body = @request_body.deep_dup + invalid_request_body[:data][:attributes].delete(:data) + post api_v1_team_inventory_column_stock_unit_items_path( + team_id: @teams.first.id, + inventory_id: @valid_inventory.id, + column_id: @stock_column + ), params: invalid_request_body.to_json, headers: @valid_headers + expect(response).to have_http_status(400) + expect { hash_body = json }.to_not raise_exception + expect(hash_body['errors'][0]).to include('status': 400) + end + end + + describe 'PUT inventory_stock_unit_item, #update' do + before :all do + @valid_headers['Content-Type'] = 'application/vnd.api+json' + @request_body = { + data: { + id: @stock_column.repository_stock_unit_items.first.id, + type: 'inventory_stock_unit_items', + attributes: { data: 'Updated' } + } + } + end + + it 'Response with correct inventory stock unit item' do + hash_body = nil + item_id = @stock_column.repository_stock_unit_items.first.id + put api_v1_team_inventory_column_stock_unit_item_path( + id: item_id, + team_id: @teams.first.id, + inventory_id: @valid_inventory.id, + column_id: @stock_column + ), params: @request_body.to_json, headers: @valid_headers + expect(response).to have_http_status 200 + expect { hash_body = json }.not_to raise_exception + expect(hash_body[:data]).to match( + JSON.parse( + ActiveModelSerializers::SerializableResource + .new(@stock_column.repository_stock_unit_items.find(item_id), serializer: Api::V1::InventoryStockUnitItemSerializer) + .to_json + )['data'] + ) + expect(@stock_column.repository_stock_unit_items.find(item_id).data).to match('Updated') + end + + it 'When invalid request, incorrect type' do + invalid_request_body = @request_body.deep_dup + invalid_request_body[:data][:type] = 'repository_rows' + hash_body = nil + put api_v1_team_inventory_column_stock_unit_item_path( + id: @stock_column.repository_stock_unit_items.first.id, + team_id: @teams.first.id, + inventory_id: @valid_inventory.id, + column_id: @stock_column + ), params: invalid_request_body.to_json, headers: @valid_headers + expect(response).to have_http_status(400) + expect { hash_body = json }.to_not raise_exception + expect(hash_body['errors'][0]).to include('status': 400) + end + + it 'When invalid request, missing attributes values' do + hash_body = nil + invalid_request_body = @request_body.deep_dup + invalid_request_body[:data][:attributes].delete(:data) + put api_v1_team_inventory_column_stock_unit_item_path( + id: @stock_column.repository_stock_unit_items.first.id, + team_id: @teams.first.id, + inventory_id: @valid_inventory.id, + column_id: @stock_column + ), params: invalid_request_body.to_json, headers: @valid_headers + expect(response).to have_http_status(400) + expect { hash_body = json }.to_not raise_exception + expect(hash_body['errors'][0]).to include('status': 400) + end + + it 'When invalid request, non existing item' do + hash_body = nil + invalid_request_body = @request_body.deep_dup + invalid_request_body[:id] = 999 + put api_v1_team_inventory_column_stock_unit_item_path( + id: 999, + team_id: @teams.first.id, + inventory_id: @valid_inventory.id, + column_id: @stock_column + ), params: invalid_request_body.to_json, headers: @valid_headers + expect(response).to have_http_status(404) + expect { hash_body = json }.to_not raise_exception + expect(hash_body['errors'][0]).to include('status': 404) + end + end + + describe 'DELETE inventory_stock_unit_item, #destroy' do + it 'Destroys inventory stock unit item' do + deleted_id = @stock_column.repository_stock_unit_items.last.id + delete api_v1_team_inventory_column_stock_unit_item_path( + id: deleted_id, + team_id: @teams.first.id, + inventory_id: @valid_inventory.id, + column_id: @stock_column.id + ), headers: @valid_headers + expect(response).to have_http_status(200) + expect(RepositoryStockUnitItem.where(id: deleted_id)).to_not exist + end + + it 'Invalid request, non existing inventory stock unit item' do + delete api_v1_team_inventory_column_stock_unit_item_path( + id: 1001, + team_id: @teams.first.id, + inventory_id: @valid_inventory.id, + column_id: @stock_column.id + ), headers: @valid_headers + expect(response).to have_http_status(404) + end + + it 'When invalid request, incorrect repository' do + deleted_id = @stock_column.repository_stock_unit_items.last.id + delete api_v1_team_inventory_column_stock_unit_item_path( + id: deleted_id, + team_id: @teams.first.id, + inventory_id: 9999, + column_id: @stock_column.id + ), headers: @valid_headers + expect(response).to have_http_status(404) + expect(RepositoryStockUnitItem.where(id: deleted_id)).to exist + end + + it 'When invalid request, repository from another team' do + deleted_id = @stock_column.repository_stock_unit_items.last.id + delete api_v1_team_inventory_column_stock_unit_item_path( + id: deleted_id, + team_id: @teams.second.id, + inventory_id: @valid_inventory.id, + column_id: @stock_column.id + ), headers: @valid_headers + expect(response).to have_http_status(403) + expect(RepositoryStockUnitItem.where(id: deleted_id)).to exist + end + end +end