diff --git a/app/controllers/api/v1/feed_movements_controller.rb b/app/controllers/api/v1/feed_movements_controller.rb new file mode 100644 index 0000000..b3bf65a --- /dev/null +++ b/app/controllers/api/v1/feed_movements_controller.rb @@ -0,0 +1,48 @@ +module Api + module V1 + class FeedMovementsController < BaseController + before_action :authenticate_request! + + def create_entry + movement = FeedMovements::CreateEntry.call( + feed_id: params[:feed_id], + quantity: entry_params[:quantity], + value: entry_params[:value], + occurred_on: entry_params[:occurred_on] + ) + render json: movement, serializer: FeedMovementSerializer, include_feed_id: true, status: :created + end + + def create_exit + movement = FeedMovements::CreateExit.call( + feed_id: params[:feed_id], + quantity: exit_params[:quantity], + reason: exit_params[:reason], + occurred_on: exit_params[:occurred_on] + ) + render json: movement, serializer: FeedMovementSerializer, include_feed_id: true, status: :created + end + + def index + feed = Feed.active.find(params[:feed_id]) + result = FeedMovements::History.call( + feed_id: feed.id, + page: params[:page], per_page: params[:per_page] + ) + + paginated = PaginatedCollectionSerializer.call(result:, serializer: FeedMovementSerializer) + render json: { feed: FeedSummarySerializer.new(feed).attributes, **paginated } + end + + private + + def entry_params + params.require(:entry).permit(:quantity, :value, :occurred_on) + end + + def exit_params + params.require(:exit).permit(:quantity, :reason, :occurred_on) + end + end + end +end diff --git a/app/models/feed.rb b/app/models/feed.rb index 7ccd502..82250d8 100644 --- a/app/models/feed.rb +++ b/app/models/feed.rb @@ -1,4 +1,5 @@ class Feed < ApplicationRecord + has_many :feed_movements, dependent: :restrict_with_error validates :name, presence: true validates :stock_quantity, presence: true, numericality: { greater_than_or_equal_to: 0 } diff --git a/app/models/feed_movement.rb b/app/models/feed_movement.rb new file mode 100644 index 0000000..4b9b91b --- /dev/null +++ b/app/models/feed_movement.rb @@ -0,0 +1,14 @@ +class FeedMovement < ApplicationRecord + belongs_to :feed + + enum :movement_type, { + entry: "entry", + exit: "exit" + }, validate: true + + validates :movement_type, presence: true + validates :quantity, presence: true, numericality: { greater_than: 0 } + validates :value, presence: true, numericality: { greater_than_or_equal_to: 0 }, if: :entry? + validates :reason, presence: true, if: :exit? + validates :occurred_on, presence: true +end diff --git a/app/serializers/feed_movement_serializer.rb b/app/serializers/feed_movement_serializer.rb new file mode 100644 index 0000000..054e41e --- /dev/null +++ b/app/serializers/feed_movement_serializer.rb @@ -0,0 +1,12 @@ +class FeedMovementSerializer < ActiveModel::Serializer + attribute :id + attribute :feed_id, if: :include_feed_id? + attributes :movement_type, :quantity + attribute :value, if: :entry? + attribute :reason, if: :exit? + attributes :occurred_on, :created_at, :updated_at + + def include_feed_id? = instance_options[:include_feed_id] + def entry? = object.entry? + def exit? = object.exit? +end diff --git a/app/serializers/feed_summary_serializer.rb b/app/serializers/feed_summary_serializer.rb new file mode 100644 index 0000000..11f93b9 --- /dev/null +++ b/app/serializers/feed_summary_serializer.rb @@ -0,0 +1,3 @@ +class FeedSummarySerializer < ActiveModel::Serializer + attributes :id, :name, :stock_quantity +end diff --git a/app/services/feed_movements/create_entry.rb b/app/services/feed_movements/create_entry.rb new file mode 100644 index 0000000..aa81d6f --- /dev/null +++ b/app/services/feed_movements/create_entry.rb @@ -0,0 +1,23 @@ +module FeedMovements + class CreateEntry + class << self + def call(feed_id:, quantity:, value:, occurred_on:) + ActiveRecord::Base.transaction do + feed = Feed.active.lock.find(feed_id) + + movement = FeedMovement.create!( + feed:, + movement_type: :entry, + quantity:, + value:, + occurred_on: + ) + + feed.update!(stock_quantity: feed.stock_quantity + quantity) + + movement + end + end + end + end +end diff --git a/app/services/feed_movements/create_exit.rb b/app/services/feed_movements/create_exit.rb new file mode 100644 index 0000000..434793d --- /dev/null +++ b/app/services/feed_movements/create_exit.rb @@ -0,0 +1,38 @@ +module FeedMovements + class CreateExit + class << self + def call(feed_id:, quantity:, reason:, occurred_on:) + ActiveRecord::Base.transaction do + feed = Feed.active.lock.find(feed_id) + + movement = FeedMovement.new( + feed:, + movement_type: :exit, + quantity:, + reason:, + occurred_on: + ) + + movement.validate! + + validate_stock!(feed:, quantity: movement.quantity) + + movement.save! + + feed.update!(stock_quantity: feed.stock_quantity - movement.quantity) + + movement + end + end + + private + + def validate_stock!(feed:, quantity:) + return if feed.stock_quantity >= quantity + + feed.errors.add(:stock_quantity, I18n.t("errors.feed_movements.insufficient_stock")) + raise ActiveRecord::RecordInvalid.new(feed) + end + end + end +end diff --git a/app/services/feed_movements/history.rb b/app/services/feed_movements/history.rb new file mode 100644 index 0000000..54a2091 --- /dev/null +++ b/app/services/feed_movements/history.rb @@ -0,0 +1,15 @@ +module FeedMovements + class History + class << self + def call(feed_id:, page:, per_page:) + feed = Feeds::Find.call(id: feed_id) + scope = feed.feed_movements.order(occurred_on: :desc, created_at: :desc) + Pagination::Paginator.call( + scope:, + page:, + per_page: + ) + end + end + end +end diff --git a/config/locales/en.yml b/config/locales/en.yml index 5939c38..06a0ce8 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -17,6 +17,8 @@ en: inactive_terrain: "The terrain is inactive." same_terrain: "The group is already in this terrain." terrain_occupied: "The terrain is occupied." + feed_movements: + insufficient_stock: "Insufficient stock for the requested quantity." activerecord: models: user: @@ -31,6 +33,9 @@ en: feed: one: "Feed" other: "Feeds" + feed_movement: + one: "Feed Movement" + other: "Feed Movements" attributes: user: name: "Name" @@ -47,3 +52,10 @@ en: feed: name: "Name" stock_quantity: "Stock Quantity" + feed_movement: + feed: "Feed" + movement_type: "Movement Type" + quantity: "Quantity" + value: "Value" + reason: "Reason" + occurred_on: "Occurred On" diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml index bf65159..c8c9bf3 100644 --- a/config/locales/pt-BR.yml +++ b/config/locales/pt-BR.yml @@ -17,6 +17,8 @@ pt-BR: inactive_terrain: "O terreno está inativo." same_terrain: "O grupo já está neste terreno." terrain_occupied: "O terreno está ocupado." + feed_movements: + insufficient_stock: "Estoque insuficiente para a quantidade solicitada." activerecord: errors: messages: @@ -36,6 +38,9 @@ pt-BR: feed: one: "Ração" other: "Rações" + feed_movement: + one: "Movimentação de Ração" + other: "Movimentações de Ração" attributes: user: name: "Nome" @@ -52,3 +57,10 @@ pt-BR: feed: name: "Nome" stock_quantity: "Quantidade em Estoque" + feed_movement: + feed: "Ração" + movement_type: "Tipo de Movimentação" + quantity: "Quantidade" + value: "Valor" + reason: "Motivo" + occurred_on: "Data da Ocorrência" diff --git a/config/routes.rb b/config/routes.rb index e3f14e4..9838586 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -27,7 +27,11 @@ resources :occupations, only: [ :create ] - resources :feeds, only: [ :create, :index, :show, :update, :destroy ] + resources :feeds, only: [ :create, :index, :show, :update, :destroy ] do + post "movements/entry", to: "feed_movements#create_entry" + post "movements/exit", to: "feed_movements#create_exit" + get "movements", to: "feed_movements#index" + end get "health", to: "health#show" end diff --git a/db/migrate/20261024125351_create_feed_movements.rb b/db/migrate/20261024125351_create_feed_movements.rb new file mode 100644 index 0000000..1cd05b2 --- /dev/null +++ b/db/migrate/20261024125351_create_feed_movements.rb @@ -0,0 +1,17 @@ +class CreateFeedMovements < ActiveRecord::Migration[8.1] + def change + create_table :feed_movements, id: :uuid do |t| + t.references :feed, null: false, foreign_key: true, type: :uuid + t.string :movement_type, null: false + t.decimal :quantity, precision: 12, scale: 2, null: false + t.decimal :value, precision: 12, scale: 2 + t.string :reason + t.date :occurred_on, null: false + + t.timestamps + end + + add_index :feed_movements, :movement_type + add_index :feed_movements, :occurred_on + end +end diff --git a/db/schema.rb b/db/schema.rb index 5574f05..0c080fb 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,11 +10,25 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.1].define(version: 2026_10_24_090253) do +ActiveRecord::Schema[8.1].define(version: 2026_10_24_125351) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" enable_extension "pgcrypto" + create_table "feed_movements", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + t.uuid "feed_id", null: false + t.string "movement_type", null: false + t.decimal "quantity", precision: 12, scale: 2, null: false + t.decimal "value", precision: 12, scale: 2 + t.string "reason" + t.date "occurred_on", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["feed_id"], name: "index_feed_movements_on_feed_id" + t.index ["movement_type"], name: "index_feed_movements_on_movement_type" + t.index ["occurred_on"], name: "index_feed_movements_on_occurred_on" + end + create_table "feeds", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| t.string "name", null: false t.decimal "stock_quantity", precision: 12, scale: 2, default: "0.0", null: false @@ -80,6 +94,7 @@ t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true end + add_foreign_key "feed_movements", "feeds" add_foreign_key "occupations", "groups" add_foreign_key "occupations", "terrains" add_foreign_key "sessions", "users" diff --git a/spec/factories/feed_movements.rb b/spec/factories/feed_movements.rb new file mode 100644 index 0000000..01149bb --- /dev/null +++ b/spec/factories/feed_movements.rb @@ -0,0 +1,21 @@ +FactoryBot.define do + factory :feed_movement do + association :feed + movement_type { :entry } + quantity { Faker::Number.decimal(l_digits: 2, r_digits: 2) } + value { Faker::Number.decimal(l_digits: 3, r_digits: 2) } + occurred_on { Faker::Date.backward(days: 30) } + + trait :entry do + movement_type { :entry } + value { Faker::Number.decimal(l_digits: 3, r_digits: 2) } + reason { nil } + end + + trait :exit do + movement_type { :exit } + value { nil } + reason { Faker::Lorem.sentence } + end + end +end diff --git a/spec/models/feed_movement_spec.rb b/spec/models/feed_movement_spec.rb new file mode 100644 index 0000000..113b4c7 --- /dev/null +++ b/spec/models/feed_movement_spec.rb @@ -0,0 +1,64 @@ +require 'rails_helper' + +RSpec.describe FeedMovement, type: :model do + describe 'validations' do + it "is valid with valid entry attributes" do + movement = build(:feed_movement, :entry) + expect(movement).to be_valid + end + + it "is valid with valid exit attributes" do + movement = build(:feed_movement, :exit) + expect(movement).to be_valid + end + + it "is invalid without a movement type" do + movement = build(:feed_movement, movement_type: nil) + expect(movement).to_not be_valid + end + + it "is invalid with an invalid movement type" do + movement = build(:feed_movement, movement_type: "invalid_type") + expect(movement).to_not be_valid + end + + it "is invalid without a quantity" do + movement = build(:feed_movement, quantity: nil) + expect(movement).to_not be_valid + end + + it "is invalid with a zero quantity" do + movement = build(:feed_movement, quantity: 0) + expect(movement).to_not be_valid + end + + it "is invalid with a negative quantity" do + movement = build(:feed_movement, quantity: -1) + expect(movement).to_not be_valid + end + + it "is invalid without an occurred on date" do + movement = build(:feed_movement, occurred_on: nil) + expect(movement).to_not be_valid + end + + context "when movement type is entry" do + it "is invalid without a value" do + movement = build(:feed_movement, :entry, value: nil) + expect(movement).to_not be_valid + end + + it "is invalid with a negative value" do + movement = build(:feed_movement, :entry, value: -1) + expect(movement).to_not be_valid + end + end + + context "when movement type is exit" do + it "is invalid without a reason" do + movement = build(:feed_movement, :exit, reason: nil) + expect(movement).to_not be_valid + end + end + end +end diff --git a/spec/requests/api/v1/feed_movements/create_entry_spec.rb b/spec/requests/api/v1/feed_movements/create_entry_spec.rb new file mode 100644 index 0000000..128a5d9 --- /dev/null +++ b/spec/requests/api/v1/feed_movements/create_entry_spec.rb @@ -0,0 +1,65 @@ +require 'rails_helper' + +RSpec.describe "POST /api/v1/feeds/:feed_id/movements/entry", type: :request do + let (:user) { create(:user) } + let (:tokens) { Auth::CreateSession.call(user:) } + + let(:headers) do + { + "Authorization" => "Bearer #{tokens[:access_token]}" + } + end + + it "creates a entry movement for the feed" do + feed = create(:feed, stock_quantity: 100) + post "/api/v1/feeds/#{feed.id}/movements/entry", params: { entry: { + quantity: 50, + value: 200, + occurred_on: Date.current + } }, headers: headers, as: :json + + expect(response).to have_http_status(:created) + body = response.parsed_body + expect(body).to include( + "feed_id" => feed.id, + "movement_type" => "entry", + "quantity" => "50.0", + "value" => "200.0", + "occurred_on" => Date.current.to_s + ) + expect(feed.reload.stock_quantity).to eq(150) + end + + it "returns unprocessable content when attributes are invalid" do + feed = create(:feed, stock_quantity: 100) + post "/api/v1/feeds/#{feed.id}/movements/entry", params: { entry: { + quantity: -50, + value: 200, + occurred_on: Date.current + } }, headers: headers, as: :json + + expect(response).to have_http_status(:unprocessable_content) + expect(feed.reload.stock_quantity).to eq(100) + end + + it "returns not found when the feed does not exist" do + post "/api/v1/feeds/#{SecureRandom.uuid}/movements/entry", params: { entry: { + quantity: 50, + value: 200, + occurred_on: Date.current + } }, headers: headers, as: :json + + expect(response).to have_http_status(:not_found) + end + + it "returns unauthorized when no token is provided" do + feed = create(:feed, stock_quantity: 100) + post "/api/v1/feeds/#{feed.id}/movements/entry", params: { entry: { + quantity: 50, + value: 200, + occurred_on: Date.current + } }, as: :json + + expect(response).to have_http_status(:unauthorized) + end +end diff --git a/spec/requests/api/v1/feed_movements/create_exit_spec.rb b/spec/requests/api/v1/feed_movements/create_exit_spec.rb new file mode 100644 index 0000000..4c1ef45 --- /dev/null +++ b/spec/requests/api/v1/feed_movements/create_exit_spec.rb @@ -0,0 +1,77 @@ +require 'rails_helper' + +RSpec.describe "POST /api/v1/feeds/:feed_id/movements/exit", type: :request do + let (:user) { create(:user) } + let (:tokens) { Auth::CreateSession.call(user:) } + + let(:headers) do + { + "Authorization" => "Bearer #{tokens[:access_token]}" + } + end + + it "creates an exit movement for a feed" do + feed = create(:feed, stock_quantity: 100) + post "/api/v1/feeds/#{feed.id}/movements/exit", params: { exit: { + quantity: 50, + reason: "Sold", + occurred_on: Date.current + } }, headers: headers, as: :json + + expect(response).to have_http_status(:created) + body = response.parsed_body + expect(body).to include( + "feed_id" => feed.id, + "movement_type" => "exit", + "quantity" => "50.0", + "reason" => "Sold", + "occurred_on" => Date.current.to_s + ) + expect(feed.reload.stock_quantity).to eq(50) + end + + it "returns unprocessable content when attributes are invalid" do + feed = create(:feed, stock_quantity: 100) + post "/api/v1/feeds/#{feed.id}/movements/exit", params: { exit: { + quantity: 150, + reason: "Sold", + occurred_on: Date.current + } }, headers: headers, as: :json + + expect(response).to have_http_status(:unprocessable_content) + expect(feed.reload.stock_quantity).to eq(100) + end + + it "returns unprocessable content when stock quantity is insufficient" do + feed = create(:feed, stock_quantity: 30) + post "/api/v1/feeds/#{feed.id}/movements/exit", params: { exit: { + quantity: 50, + reason: "Sold", + occurred_on: Date.current + } }, headers: headers, as: :json + + expect(response).to have_http_status(:unprocessable_content) + expect(feed.reload.stock_quantity).to eq(30) + end + + it "returns not found when the feed does not exist" do + post "/api/v1/feeds/#{SecureRandom.uuid}/movements/exit", params: { exit: { + quantity: 50, + reason: "Sold", + occurred_on: Date.current + } }, headers: headers, as: :json + + expect(response).to have_http_status(:not_found) + end + + it "returns unauthorized when no token is provided" do + feed = create(:feed, stock_quantity: 100) + post "/api/v1/feeds/#{feed.id}/movements/exit", params: { exit: { + quantity: 50, + reason: "Sold", + occurred_on: Date.current + } }, as: :json + + expect(response).to have_http_status(:unauthorized) + end +end diff --git a/spec/requests/api/v1/feed_movements/index_spec.rb b/spec/requests/api/v1/feed_movements/index_spec.rb new file mode 100644 index 0000000..7432595 --- /dev/null +++ b/spec/requests/api/v1/feed_movements/index_spec.rb @@ -0,0 +1,49 @@ +require 'rails_helper' + +RSpec.describe "GET /api/v1/feeds/:feed_id/movements", type: :request do + let (:user) { create(:user) } + let (:tokens) { Auth::CreateSession.call(user:) } + + let(:headers) do + { + "Authorization" => "Bearer #{tokens[:access_token]}" + } + end + + it "returns a list of feed movements for the given feed" do + feed = create(:feed, stock_quantity: 100) + create(:feed_movement, feed:, movement_type: "entry", quantity: 50, value: 200, occurred_on: Date.current) + create(:feed_movement, feed:, movement_type: "exit", quantity: 30, reason: "Sold", occurred_on: Date.current) + get "/api/v1/feeds/#{feed.id}/movements", headers: headers + body = response.parsed_body + expect(response).to have_http_status(:ok) + expect(body['data'].size).to eq(2) + expect(body['data'].pluck('movement_type')).to match_array([ "entry", "exit" ]) + end + + it "returns a paginated list of feed movements" do + feed = create(:feed, stock_quantity: 100) + create_list(:feed_movement, 3, feed:, movement_type: "entry", quantity: 50, value: 200, occurred_on: Date.current) + get "/api/v1/feeds/#{feed.id}/movements", headers: headers, params: { page: 1, per_page: 2 } + body = response.parsed_body + expect(response).to have_http_status(:ok) + expect(body['data'].size).to eq(2) + expect(body['meta']).to include( + 'page' => 1, + 'per_page' => 2, + 'total_items' => 3, + 'total_pages' => 2 + ) + end + + it "returns not found when the feed does not exist" do + get "/api/v1/feeds/#{SecureRandom.uuid}/movements", headers: headers + expect(response).to have_http_status(:not_found) + end + + it "returns unauthorized when no token is provided" do + feed = create(:feed, stock_quantity: 100) + get "/api/v1/feeds/#{feed.id}/movements" + expect(response).to have_http_status(:unauthorized) + end +end diff --git a/spec/services/feed_movements/create_entry_spec.rb b/spec/services/feed_movements/create_entry_spec.rb new file mode 100644 index 0000000..82522dc --- /dev/null +++ b/spec/services/feed_movements/create_entry_spec.rb @@ -0,0 +1,58 @@ +require 'rails_helper' + +RSpec.describe FeedMovements::CreateEntry, type: :service do + describe '.call' do + it "creates a entry movement" do + feed = create(:feed, stock_quantity: 100) + movement = described_class.call( + feed_id: feed.id, + quantity: 50, + value: 200, + occurred_on: Date.current + ) + expect(movement).to be_persisted + expect(movement).to be_entry + expect(movement.feed).to eq(feed) + expect(movement.quantity).to eq(50) + expect(movement.value).to eq(200) + expect(movement.occurred_on).to eq(Date.current) + end + + it "increases the feed stock quantity" do + feed = create(:feed, stock_quantity: 100) + described_class.call( + feed_id: feed.id, + quantity: 50, + value: 200, + occurred_on: Date.current + ) + expect(feed.reload.stock_quantity).to eq(150) + end + + it "does not create a movement when attributes are invalid" do + feed = create(:feed, stock_quantity: 100) + expect { + described_class.call( + feed_id: feed.id, + quantity: -50, + value: 200, + occurred_on: Date.current + ) + }.to raise_error(ActiveRecord::RecordInvalid) + expect(feed.reload.stock_quantity).to eq(100) + expect(feed.feed_movements).to be_empty + end + + it "raises an error when the feed is deleted" do + feed = create(:feed, :deleted) + expect { + described_class.call( + feed_id: feed.id, + quantity: 50, + value: 200, + occurred_on: Date.current + ) + }.to raise_error(ActiveRecord::RecordNotFound) + end + end +end diff --git a/spec/services/feed_movements/create_exit_spec.rb b/spec/services/feed_movements/create_exit_spec.rb new file mode 100644 index 0000000..f3791d8 --- /dev/null +++ b/spec/services/feed_movements/create_exit_spec.rb @@ -0,0 +1,83 @@ +require 'rails_helper' + +RSpec.describe FeedMovements::CreateExit, type: :service do + describe '.call' do + it "creates an exit movement" do + feed = create(:feed, stock_quantity: 100) + movement = described_class.call( + feed_id: feed.id, + quantity: 50, + reason: "Sold", + occurred_on: Date.current + ) + expect(movement).to be_persisted + expect(movement).to be_exit + expect(movement.feed).to eq(feed) + expect(movement.quantity).to eq(50) + expect(movement.reason).to eq("Sold") + expect(movement.occurred_on).to eq(Date.current) + end + + it "decreases the feed stock quantity" do + feed = create(:feed, stock_quantity: 100) + described_class.call( + feed_id: feed.id, + quantity: 50, + reason: "Sold", + occurred_on: Date.current + ) + expect(feed.reload.stock_quantity).to eq(50) + end + + it "allows removing the entire available stock" do + feed = create(:feed, stock_quantity: 100) + described_class.call( + feed_id: feed.id, + quantity: 100, + reason: "Sold", + occurred_on: Date.current + ) + expect(feed.reload.stock_quantity).to eq(0) + end + + it "does not allow exit movement when stock is insufficient" do + feed = create(:feed, stock_quantity: 30) + expect { + described_class.call( + feed_id: feed.id, + quantity: 50, + reason: "Sold", + occurred_on: Date.current + ) + }.to raise_error(ActiveRecord::RecordInvalid) + expect(feed.reload.stock_quantity).to eq(30) + expect(feed.feed_movements).to be_empty + end + + it "does not create a movement when attributes are invalid" do + feed = create(:feed, stock_quantity: 100) + expect { + described_class.call( + feed_id: feed.id, + quantity: -50, + reason: "Sold", + occurred_on: Date.current + ) + }.to raise_error(ActiveRecord::RecordInvalid) + expect(feed.reload.stock_quantity).to eq(100) + expect(feed.feed_movements).to be_empty + end + + it "raises an error when the feed is deleted" do + feed = create(:feed, :deleted) + expect { + described_class.call( + feed_id: feed.id, + quantity: 50, + reason: "Sold", + occurred_on: Date.current + ) + }.to raise_error(ActiveRecord::RecordNotFound) + end + end +end diff --git a/spec/services/feed_movements/history_spec.rb b/spec/services/feed_movements/history_spec.rb new file mode 100644 index 0000000..a2662a4 --- /dev/null +++ b/spec/services/feed_movements/history_spec.rb @@ -0,0 +1,24 @@ +require 'rails_helper' + +RSpec.describe FeedMovements::History do + describe '.call' do + it "returns the history of feed movements for a given feed" do + feed = create(:feed) + other_feed = create(:feed) + entry_movement = create(:feed_movement, :entry, feed:, quantity: 50, value: 200, occurred_on: Date.current - 1.day) + exit_movement = create(:feed_movement, :exit, feed:, quantity: 30, reason: "Sold", occurred_on: Date.current) + create(:feed_movement, :entry, feed: other_feed, quantity: 20, value: 100, occurred_on: Date.current) + result = described_class.call(feed_id: feed.id, page: 1, per_page: 10) + expect(result.records).to eq([ exit_movement, entry_movement ]) + end + + it "raises an error when the feed does not exist" do + expect { described_class.call(feed_id: SecureRandom.uuid, page: 1, per_page: 10) }.to raise_error(ActiveRecord::RecordNotFound) + end + + it "raises an error when the feed is deleted" do + feed = create(:feed, :deleted) + expect { described_class.call(feed_id: feed.id, page: 1, per_page: 10) }.to raise_error(ActiveRecord::RecordNotFound) + end + end +end