From b0980acd8729477d3c4b6bdc582743a03f0c75c5 Mon Sep 17 00:00:00 2001 From: Felipe Lima Date: Sat, 24 Oct 2026 06:08:49 -0300 Subject: [PATCH 01/14] =?UTF-8?q?feat:=20adiciona=20estrutura=20base=20de?= =?UTF-8?q?=20ra=C3=A7=C3=B5es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/models/feed.rb | 15 +++++++++++++++ db/migrate/20261024090253_create_feeds.rb | 13 +++++++++++++ db/schema.rb | 11 ++++++++++- spec/factories/feeds.rb | 7 +++++++ spec/models/feed_spec.rb | 5 +++++ 5 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 app/models/feed.rb create mode 100644 db/migrate/20261024090253_create_feeds.rb create mode 100644 spec/factories/feeds.rb create mode 100644 spec/models/feed_spec.rb diff --git a/app/models/feed.rb b/app/models/feed.rb new file mode 100644 index 0000000..7ccd502 --- /dev/null +++ b/app/models/feed.rb @@ -0,0 +1,15 @@ +class Feed < ApplicationRecord + validates :name, presence: true + validates :stock_quantity, presence: true, numericality: { greater_than_or_equal_to: 0 } + + scope :active, -> { where(deleted_at: nil) } + scope :deleted, -> { where.not(deleted_at: nil) } + + def deleted? + deleted_at.present? + end + + def soft_delete! + update!(deleted_at: Time.current) + end +end diff --git a/db/migrate/20261024090253_create_feeds.rb b/db/migrate/20261024090253_create_feeds.rb new file mode 100644 index 0000000..78af75a --- /dev/null +++ b/db/migrate/20261024090253_create_feeds.rb @@ -0,0 +1,13 @@ +class CreateFeeds < ActiveRecord::Migration[8.1] + def change + create_table :feeds, id: :uuid do |t| + t.string :name, null: false + t.decimal :stock_quantity, precision: 12, scale: 2, null: false, default: 0 + t.datetime :deleted_at + + t.timestamps + end + + add_index :feeds, :deleted_at + end +end diff --git a/db/schema.rb b/db/schema.rb index c4d8d3c..5574f05 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,11 +10,20 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.1].define(version: 2026_10_22_181113) do +ActiveRecord::Schema[8.1].define(version: 2026_10_24_090253) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" enable_extension "pgcrypto" + 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 + t.datetime "deleted_at" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["deleted_at"], name: "index_feeds_on_deleted_at" + end + create_table "groups", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| t.string "name", null: false t.integer "animal_count", null: false diff --git a/spec/factories/feeds.rb b/spec/factories/feeds.rb new file mode 100644 index 0000000..46c1e67 --- /dev/null +++ b/spec/factories/feeds.rb @@ -0,0 +1,7 @@ +FactoryBot.define do + factory :feed do + name { "MyString" } + stock_quantity { "9.99" } + deleted_at { "2026-10-24 06:02:55" } + end +end diff --git a/spec/models/feed_spec.rb b/spec/models/feed_spec.rb new file mode 100644 index 0000000..c46ce38 --- /dev/null +++ b/spec/models/feed_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe Feed, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end From a2a45575349097d81070af98f280afd188982205 Mon Sep 17 00:00:00 2001 From: Felipe Lima Date: Sat, 24 Oct 2026 06:17:48 -0300 Subject: [PATCH 02/14] =?UTF-8?q?test:=20adiciona=20cobertura=20do=20model?= =?UTF-8?q?=20de=20ra=C3=A7=C3=B5es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spec/factories/feeds.rb | 10 ++++--- spec/models/feed_spec.rb | 58 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 64 insertions(+), 4 deletions(-) diff --git a/spec/factories/feeds.rb b/spec/factories/feeds.rb index 46c1e67..c3583d5 100644 --- a/spec/factories/feeds.rb +++ b/spec/factories/feeds.rb @@ -1,7 +1,11 @@ FactoryBot.define do factory :feed do - name { "MyString" } - stock_quantity { "9.99" } - deleted_at { "2026-10-24 06:02:55" } + name { Faker::Food.ingredient } + stock_quantity { Faker::Number.decimal(l_digits: 3, r_digits: 2) } + deleted_at { nil } + + trait :deleted do + deleted_at { Time.current } + end end end diff --git a/spec/models/feed_spec.rb b/spec/models/feed_spec.rb index c46ce38..acb146d 100644 --- a/spec/models/feed_spec.rb +++ b/spec/models/feed_spec.rb @@ -1,5 +1,61 @@ require 'rails_helper' RSpec.describe Feed, type: :model do - pending "add some examples to (or delete) #{__FILE__}" + describe 'validations' do + it "is valid with valid attributes" do + feed = build(:feed) + expect(feed).to be_valid + end + + it "is not valid without a name" do + feed = build(:feed, name: nil) + expect(feed).not_to be_valid + end + + it "is not valid without a stock_quantity" do + feed = build(:feed, stock_quantity: nil) + expect(feed).not_to be_valid + end + + it "is not valid with a negative stock_quantity" do + feed = build(:feed, stock_quantity: -1.0) + expect(feed).not_to be_valid + end + end + + describe '.active' do + it "returns only active feeds" do + active_feed = create(:feed) + create(:feed, :deleted) + expect(described_class.active).to contain_exactly(active_feed) + end + end + + describe '.deleted' do + it "returns only deleted feeds" do + create(:feed) + deleted_feed = create(:feed, :deleted) + expect(described_class.deleted).to contain_exactly(deleted_feed) + end + end + + describe '#deleted?' do + it "returns true if the feed is deleted" do + feed = build(:feed, :deleted) + expect(feed.deleted?).to be true + end + + it "returns false if the feed is not deleted" do + feed = build(:feed) + expect(feed.deleted?).to be false + end + end + + describe '#soft_delete!' do + it "marks the feed as deleted" do + feed = create(:feed) + feed.soft_delete! + expect(feed.deleted?).to be true + end + end end From 383fc9b7f2825872e89b219f90e939e75da23c1a Mon Sep 17 00:00:00 2001 From: Felipe Lima Date: Sat, 24 Oct 2026 07:28:18 -0300 Subject: [PATCH 03/14] =?UTF-8?q?feat:=20adiciona=20cria=C3=A7=C3=A3o=20de?= =?UTF-8?q?=20ra=C3=A7=C3=B5es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/services/feeds/create.rb | 9 +++++++++ spec/services/feeds/create_spec.rb | 18 ++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 app/services/feeds/create.rb create mode 100644 spec/services/feeds/create_spec.rb diff --git a/app/services/feeds/create.rb b/app/services/feeds/create.rb new file mode 100644 index 0000000..c233481 --- /dev/null +++ b/app/services/feeds/create.rb @@ -0,0 +1,9 @@ +module Feeds + class Create + class << self + def call(name:, initial_stock_quantity:) + Feed.create!(name:, stock_quantity: initial_stock_quantity) + end + end + end +end diff --git a/spec/services/feeds/create_spec.rb b/spec/services/feeds/create_spec.rb new file mode 100644 index 0000000..b24ab3c --- /dev/null +++ b/spec/services/feeds/create_spec.rb @@ -0,0 +1,18 @@ +require 'rails_helper' + +RSpec.describe Feeds::Create, type: :service do + describe '.call' do + it "creates a feed with initial stock quantity" do + feed = described_class.call(name: "Feed A", initial_stock_quantity: 100) + expect(feed).to be_persisted + expect(feed.name).to eq("Feed A") + expect(feed.stock_quantity).to eq(100) + end + + it "raises an error if the feed cannot be created" do + expect { + described_class.call(name: nil, initial_stock_quantity: 100) + }.to raise_error(ActiveRecord::RecordInvalid) + end + end +end From 400f3a88ecb269f1c1df36ae60e86abda11c9e4a Mon Sep 17 00:00:00 2001 From: Felipe Lima Date: Sat, 24 Oct 2026 07:52:06 -0300 Subject: [PATCH 04/14] =?UTF-8?q?feat:=20adiciona=20endpoint=20de=20cria?= =?UTF-8?q?=C3=A7=C3=A3o=20de=20ra=C3=A7=C3=B5es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controllers/api/v1/feeds_controller.rb | 18 +++++++++++ app/serializers/feed_serializer.rb | 3 ++ config/routes.rb | 2 ++ spec/requests/api/v1/feeds/create_spec.rb | 35 ++++++++++++++++++++++ 4 files changed, 58 insertions(+) create mode 100644 app/controllers/api/v1/feeds_controller.rb create mode 100644 app/serializers/feed_serializer.rb create mode 100644 spec/requests/api/v1/feeds/create_spec.rb diff --git a/app/controllers/api/v1/feeds_controller.rb b/app/controllers/api/v1/feeds_controller.rb new file mode 100644 index 0000000..487c570 --- /dev/null +++ b/app/controllers/api/v1/feeds_controller.rb @@ -0,0 +1,18 @@ +module Api + module V1 + class FeedsController < BaseController + before_action :authenticate_request! + + def create + feed = Feeds::Create.call(name: feed_params[:name], initial_stock_quantity: feed_params[:initial_stock_quantity]) + render json: feed, serializer: FeedSerializer, status: :created + end + + private + + def feed_params + params.require(:feed).permit(:name, :initial_stock_quantity) + end + end + end +end diff --git a/app/serializers/feed_serializer.rb b/app/serializers/feed_serializer.rb new file mode 100644 index 0000000..7ddc67f --- /dev/null +++ b/app/serializers/feed_serializer.rb @@ -0,0 +1,3 @@ +class FeedSerializer < ActiveModel::Serializer + attributes :id, :name, :stock_quantity, :created_at, :updated_at +end diff --git a/config/routes.rb b/config/routes.rb index 080a0a7..e35712d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -27,6 +27,8 @@ resources :occupations, only: [ :create ] + resources :feeds, only: [ :create ] + get "health", to: "health#show" end end diff --git a/spec/requests/api/v1/feeds/create_spec.rb b/spec/requests/api/v1/feeds/create_spec.rb new file mode 100644 index 0000000..0de2756 --- /dev/null +++ b/spec/requests/api/v1/feeds/create_spec.rb @@ -0,0 +1,35 @@ +require 'rails_helper' + +RSpec.describe "POST /api/v1/feeds", type: :request do + let (:user) { create(:user) } + let (:tokens) { Auth::CreateSession.call(user:) } + + let(:headers) do + { + "Authorization" => "Bearer #{tokens[:access_token]}" + } + end + + let(:valid_params) { { feed: { name: "Feed A", initial_stock_quantity: 100.5 } } } + + it "creates a feed with valid attributes" do + post "/api/v1/feeds", params: valid_params, headers: headers, as: :json + expect(response).to have_http_status(:created) + expect(response.parsed_body).to include( + "id" => be_present, + "name" => "Feed A", + "stock_quantity" => "100.5", + ) + end + + it "returns validation errors with invalid attributes" do + post "/api/v1/feeds", params: { feed: { name: nil, initial_stock_quantity: -100 } }, headers: headers, as: :json + expect(response).to have_http_status(:unprocessable_content) + expect(response.parsed_body["errors"]["name"]).to be_present + end + + it "returns unauthorized without valid authentication" do + post "/api/v1/feeds", params: valid_params, as: :json + expect(response).to have_http_status(:unauthorized) + end +end From 477f6c4faf1e5a04f2426e731345622e1c703995 Mon Sep 17 00:00:00 2001 From: Felipe Lima Date: Sat, 24 Oct 2026 07:58:38 -0300 Subject: [PATCH 05/14] =?UTF-8?q?feat:=20adiciona=20busca=20de=20ra=C3=A7?= =?UTF-8?q?=C3=A3o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/services/feeds/find.rb | 9 +++++++++ spec/services/feeds/find_spec.rb | 20 ++++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 app/services/feeds/find.rb create mode 100644 spec/services/feeds/find_spec.rb diff --git a/app/services/feeds/find.rb b/app/services/feeds/find.rb new file mode 100644 index 0000000..37df0fb --- /dev/null +++ b/app/services/feeds/find.rb @@ -0,0 +1,9 @@ +module Feeds + class Find + class << self + def call(id:) + Feed.active.find(id) + end + end + end +end diff --git a/spec/services/feeds/find_spec.rb b/spec/services/feeds/find_spec.rb new file mode 100644 index 0000000..643d4e8 --- /dev/null +++ b/spec/services/feeds/find_spec.rb @@ -0,0 +1,20 @@ +require 'rails_helper' + +RSpec.describe Feeds::Find, type: :service do + describe '.call' do + it 'returns an active feed' do + feed = create(:feed) + result = described_class.call(id: feed.id) + expect(result).to eq(feed) + end + + it 'raises an error when feed deleted' do + feed = create(:feed, :deleted) + expect { described_class.call(id: feed.id) }.to raise_error(ActiveRecord::RecordNotFound) + end + + it 'raises an error when feed does not exist' do + expect { described_class.call(id: SecureRandom.uuid) }.to raise_error(ActiveRecord::RecordNotFound) + end + end +end From 1a7a50779d9fc19b5df13817f6c59855402353a2 Mon Sep 17 00:00:00 2001 From: Felipe Lima Date: Sat, 24 Oct 2026 08:07:53 -0300 Subject: [PATCH 06/14] =?UTF-8?q?feat:=20adiciona=20endpoint=20de=20consul?= =?UTF-8?q?ta=20de=20ra=C3=A7=C3=A3o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controllers/api/v1/feeds_controller.rb | 5 ++++ config/routes.rb | 2 +- spec/requests/api/v1/feeds/show_spec.rb | 35 ++++++++++++++++++++++ 3 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 spec/requests/api/v1/feeds/show_spec.rb diff --git a/app/controllers/api/v1/feeds_controller.rb b/app/controllers/api/v1/feeds_controller.rb index 487c570..c6aae18 100644 --- a/app/controllers/api/v1/feeds_controller.rb +++ b/app/controllers/api/v1/feeds_controller.rb @@ -8,6 +8,11 @@ def create render json: feed, serializer: FeedSerializer, status: :created end + def show + feed = Feeds::Find.call(id: params[:id]) + render json: feed, serializer: FeedSerializer + end + private def feed_params diff --git a/config/routes.rb b/config/routes.rb index e35712d..09f12ae 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -27,7 +27,7 @@ resources :occupations, only: [ :create ] - resources :feeds, only: [ :create ] + resources :feeds, only: [ :create, :show ] get "health", to: "health#show" end diff --git a/spec/requests/api/v1/feeds/show_spec.rb b/spec/requests/api/v1/feeds/show_spec.rb new file mode 100644 index 0000000..765a254 --- /dev/null +++ b/spec/requests/api/v1/feeds/show_spec.rb @@ -0,0 +1,35 @@ +require 'rails_helper' + +RSpec.describe "GET /api/v1/feeds/:id", 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 feed with the given id" do + feed = create(:feed) + get "/api/v1/feeds/#{feed.id}", headers: headers + body = response.parsed_body + expect(response).to have_http_status(:ok) + expect(body).to include( + "id" => feed.id, + "name" => feed.name, + "stock_quantity" => feed.stock_quantity.to_s("F") + ) + end + + it "returns not found when the feed does not exist" do + get "/api/v1/feeds/#{SecureRandom.uuid}", headers: headers + expect(response).to have_http_status(:not_found) + end + + it "returns unauthorized when no token is provided" do + feed = create(:feed) + get "/api/v1/feeds/#{feed.id}" + expect(response).to have_http_status(:unauthorized) + end +end From 929778769329a2db27532d6645101bae57c49b5a Mon Sep 17 00:00:00 2001 From: Felipe Lima Date: Sat, 24 Oct 2026 08:25:33 -0300 Subject: [PATCH 07/14] =?UTF-8?q?feat:=20adiciona=20filtros=20e=20ordena?= =?UTF-8?q?=C3=A7=C3=A3o=20=C3=A0=20listagem=20de=20ra=C3=A7=C3=B5es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/queries/feeds/query.rb | 24 +++++++++++++++++ app/services/feeds/list.rb | 14 ++++++++++ spec/services/feeds/list_spec.rb | 45 ++++++++++++++++++++++++++++++++ 3 files changed, 83 insertions(+) create mode 100644 app/queries/feeds/query.rb create mode 100644 app/services/feeds/list.rb create mode 100644 spec/services/feeds/list_spec.rb diff --git a/app/queries/feeds/query.rb b/app/queries/feeds/query.rb new file mode 100644 index 0000000..aac288c --- /dev/null +++ b/app/queries/feeds/query.rb @@ -0,0 +1,24 @@ +module Feeds + class Query + FILTERS = { + name: ->(scope, value) { scope.where("name ILIKE ?", "%#{value}%") }, + min_stock_quantity: ->(scope, value) { scope.where("stock_quantity >= ?", value) }, + max_stock_quantity: ->(scope, value) { scope.where("stock_quantity <= ?", value) } + }.freeze + + SORTS = { + name: :name, + stock_quantity: :stock_quantity, + created_at: :created_at + }.freeze + + DEFAULT_SORT = { updated_at: :desc }.freeze + + class << self + def call(filters: {}, sort: nil, direction: nil) + scope = Filtering::Filterer.call(scope: Feed.active, filters:, definitions: FILTERS) + Sorting::Sorter.call(scope: scope, sort:, direction:, allowed: SORTS, default: DEFAULT_SORT) + end + end + end +end diff --git a/app/services/feeds/list.rb b/app/services/feeds/list.rb new file mode 100644 index 0000000..335607e --- /dev/null +++ b/app/services/feeds/list.rb @@ -0,0 +1,14 @@ +module Feeds + class List + class << self + def call(filters: {}, sort: nil, direction: nil, page:, per_page:) + scope = Feeds::Query.call(filters:, sort:, direction:) + Pagination::Paginator.call( + scope: scope, + page:, + per_page: + ) + end + end + end +end diff --git a/spec/services/feeds/list_spec.rb b/spec/services/feeds/list_spec.rb new file mode 100644 index 0000000..c58f551 --- /dev/null +++ b/spec/services/feeds/list_spec.rb @@ -0,0 +1,45 @@ +require 'rails_helper' + +RSpec.describe Feeds::List, type: :service do + describe '.call' do + it "returns only active feeds" do + active_feed = create(:feed) + create(:feed, :deleted) + result = described_class.call(page: 1, per_page: 10) + expect(result.records).to contain_exactly(active_feed) + end + + it "filters feeds by name" do + matching_feed = create(:feed, name: "Premium Feed") + create(:feed, name: "Basic Feed") + result = described_class.call(filters: { name: "Premium" }, page: 1, per_page: 10) + expect(result.records).to contain_exactly(matching_feed) + end + + it "sorts feeds by stock quantity" do + low_stock_feed = create(:feed, stock_quantity: 5) + high_stock_feed = create(:feed, stock_quantity: 20) + result = described_class.call(sort: "stock_quantity", direction: "desc", page: 1, per_page: 10) + expect(result.records).to eq([ high_stock_feed, low_stock_feed ]) + end + + it "returns a paginated result" do + create_list(:feed, 15) + result = described_class.call(page: 2, per_page: 10) + expect(result.records.size).to eq(5) + expect(result.page).to eq(2) + expect(result.per_page).to eq(10) + expect(result.total_items).to eq(15) + expect(result.total_pages).to eq(2) + end + + it "paginates the filtered query results" do + create_list(:feed, 15, name: "Premium Feed") + create_list(:feed, 10, name: "Basic Feed") + result = described_class.call(filters: { name: "Premium" }, page: 1, per_page: 10) + expect(result.records.size).to eq(10) + expect(result.total_items).to eq(15) + expect(result.total_pages).to eq(2) + end + end +end From 3908d629d19ee6a734c62f63966c3a420ae39803 Mon Sep 17 00:00:00 2001 From: Felipe Lima Date: Sat, 24 Oct 2026 08:37:53 -0300 Subject: [PATCH 08/14] =?UTF-8?q?feat:=20adiciona=20endpoint=20de=20listag?= =?UTF-8?q?em=20de=20ra=C3=A7=C3=B5es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controllers/api/v1/feeds_controller.rb | 9 +++++ config/routes.rb | 2 +- spec/requests/api/v1/feeds/index_spec.rb | 44 ++++++++++++++++++++++ 3 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 spec/requests/api/v1/feeds/index_spec.rb diff --git a/app/controllers/api/v1/feeds_controller.rb b/app/controllers/api/v1/feeds_controller.rb index c6aae18..17f1534 100644 --- a/app/controllers/api/v1/feeds_controller.rb +++ b/app/controllers/api/v1/feeds_controller.rb @@ -8,6 +8,15 @@ def create render json: feed, serializer: FeedSerializer, status: :created end + def index + result = Feeds::List.call( + filters: params.permit(:name, :min_stock_quantity, :max_stock_quantity).to_h, + sort: params[:sort], + direction: params[:direction], + page: params[:page], per_page: params[:per_page]) + render json: PaginatedCollectionSerializer.call(result:, serializer: FeedSerializer) + end + def show feed = Feeds::Find.call(id: params[:id]) render json: feed, serializer: FeedSerializer diff --git a/config/routes.rb b/config/routes.rb index 09f12ae..de2ce80 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -27,7 +27,7 @@ resources :occupations, only: [ :create ] - resources :feeds, only: [ :create, :show ] + resources :feeds, only: [ :create, :index, :show ] get "health", to: "health#show" end diff --git a/spec/requests/api/v1/feeds/index_spec.rb b/spec/requests/api/v1/feeds/index_spec.rb new file mode 100644 index 0000000..608cac5 --- /dev/null +++ b/spec/requests/api/v1/feeds/index_spec.rb @@ -0,0 +1,44 @@ +require 'rails_helper' + +RSpec.describe "GET /api/v1/feeds", type: :request do + let (:user) { create(:user) } + let (:tokens) { Auth::CreateSession.call(user:) } + + let(:headers) do + { + "Authorization" => "Bearer #{tokens[:access_token]}" + } + end + + it "filters feeds" do + matching_feed = create(:feed, name: "Premium Feed") + create(:feed, name: "Basic Feed") + get "/api/v1/feeds", headers: headers, params: { name: "Premium" } + body = response.parsed_body + expect(response).to have_http_status(:ok) + expect(body['data'].pluck('id')).to eq([ matching_feed.id ]) + end + + it "sorts feeds" do + low_stock_feed = create(:feed, stock_quantity: 5) + high_stock_feed = create(:feed, stock_quantity: 20) + get "/api/v1/feeds", headers: headers, params: { sort: "stock_quantity", direction: "desc" } + body = response.parsed_body + expect(response).to have_http_status(:ok) + expect(body['data'].pluck('id')).to eq([ high_stock_feed.id, low_stock_feed.id ]) + end + + it "returns a paginated list of feeds" do + create_list(:feed, 3) + get "/api/v1/feeds", 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 +end From 0aabac72f7800c28e23cb6369cbbfe20e50e3688 Mon Sep 17 00:00:00 2001 From: Felipe Lima Date: Sat, 24 Oct 2026 08:47:12 -0300 Subject: [PATCH 09/14] =?UTF-8?q?feat:=20adiciona=20atualiza=C3=A7=C3=A3o?= =?UTF-8?q?=20de=20ra=C3=A7=C3=B5es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/services/feeds/update.rb | 11 +++++++++ spec/services/feeds/update_spec.rb | 37 ++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 app/services/feeds/update.rb create mode 100644 spec/services/feeds/update_spec.rb diff --git a/app/services/feeds/update.rb b/app/services/feeds/update.rb new file mode 100644 index 0000000..b096554 --- /dev/null +++ b/app/services/feeds/update.rb @@ -0,0 +1,11 @@ +module Feeds + class Update + class << self + def call(id:, name:) + feed = Feeds::Find.call(id:) + feed.update!(name:) + feed + end + end + end +end diff --git a/spec/services/feeds/update_spec.rb b/spec/services/feeds/update_spec.rb new file mode 100644 index 0000000..9ed4e4b --- /dev/null +++ b/spec/services/feeds/update_spec.rb @@ -0,0 +1,37 @@ +require 'rails_helper' + +RSpec.describe Feeds::Update, type: :service do + describe '.call' do + it 'updates the feed name' do + feed = create(:feed, name: "Old Name") + result = described_class.call(id: feed.id, name: "New Name") + expect(result.name).to eq("New Name") + end + + it "persists the changes" do + feed = create(:feed, name: "Old Name") + described_class.call(id: feed.id, name: "New Name") + expect(feed.reload.name).to eq("New Name") + end + + it "raises an error if the name is invalid" do + feed = create(:feed) + expect { + described_class.call(id: feed.id, name: "") + }.to raise_error(ActiveRecord::RecordInvalid) + end + + it 'raises an error when the feed is deleted' do + feed = create(:feed, :deleted) + expect { + described_class.call(id: feed.id, name: "New Name") + }.to raise_error(ActiveRecord::RecordNotFound) + end + + it 'raises an error when the feed does not exist' do + expect { + described_class.call(id: SecureRandom.uuid, name: "New Name") + }.to raise_error(ActiveRecord::RecordNotFound) + end + end +end From 9161c3001e0d65800c7e0ffa54a739674e8ee391 Mon Sep 17 00:00:00 2001 From: Felipe Lima Date: Sat, 24 Oct 2026 09:10:28 -0300 Subject: [PATCH 10/14] =?UTF-8?q?feat:=20adiciona=20endpoint=20de=20atuali?= =?UTF-8?q?za=C3=A7=C3=A3o=20de=20ra=C3=A7=C3=B5es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controllers/api/v1/feeds_controller.rb | 5 ++ config/routes.rb | 2 +- spec/requests/api/v1/feeds/update_spec.rb | 56 ++++++++++++++++++++++ 3 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 spec/requests/api/v1/feeds/update_spec.rb diff --git a/app/controllers/api/v1/feeds_controller.rb b/app/controllers/api/v1/feeds_controller.rb index 17f1534..a63c0f9 100644 --- a/app/controllers/api/v1/feeds_controller.rb +++ b/app/controllers/api/v1/feeds_controller.rb @@ -22,6 +22,11 @@ def show render json: feed, serializer: FeedSerializer end + def update + feed = Feeds::Update.call(id: params[:id], name: feed_params[:name]) + render json: feed, serializer: FeedSerializer + end + private def feed_params diff --git a/config/routes.rb b/config/routes.rb index de2ce80..75eb3ef 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -27,7 +27,7 @@ resources :occupations, only: [ :create ] - resources :feeds, only: [ :create, :index, :show ] + resources :feeds, only: [ :create, :index, :show, :update ] get "health", to: "health#show" end diff --git a/spec/requests/api/v1/feeds/update_spec.rb b/spec/requests/api/v1/feeds/update_spec.rb new file mode 100644 index 0000000..45214b5 --- /dev/null +++ b/spec/requests/api/v1/feeds/update_spec.rb @@ -0,0 +1,56 @@ +require 'rails_helper' + +RSpec.describe "PATCH /api/v1/feeds/:id", type: :request do + let (:user) { create(:user) } + let (:tokens) { Auth::CreateSession.call(user:) } + + let(:headers) do + { + "Authorization" => "Bearer #{tokens[:access_token]}" + } + end + + it "updates the feed name" do + feed = create(:feed, name: "Old Name") + patch "/api/v1/feeds/#{feed.id}", params: { feed: { name: "New Name" } }, headers: headers, as: :json + body = response.parsed_body + expect(response).to have_http_status(:ok) + expect(body).to include( + "id" => feed.id, + "name" => "New Name", + "stock_quantity" => feed.stock_quantity.to_s("F") + ) + expect(feed.reload.name).to eq("New Name") + end + + it "does not update the stock quantity directly" do + feed = create(:feed, stock_quantity: 10) + patch "/api/v1/feeds/#{feed.id}", params: { feed: { name: feed.name, stock_quantity: 20 } }, headers: headers, as: :json + body = response.parsed_body + expect(response).to have_http_status(:ok) + expect(body).to include( + "id" => feed.id, + "name" => feed.name, + "stock_quantity" => feed.stock_quantity.to_s("F") + ) + expect(feed.reload.stock_quantity).to eq(10) + end + + it "returns validation errors when the attributes are invalid" do + feed = create(:feed) + patch "/api/v1/feeds/#{feed.id}", params: { feed: { name: "" } }, headers: headers, as: :json + expect(response).to have_http_status(:unprocessable_content) + expect(response.parsed_body["errors"]["name"]).to be_present + end + + it "returns not found when the feed does not exist" do + patch "/api/v1/feeds/#{SecureRandom.uuid}", params: { feed: { name: "New Name" } }, 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) + patch "/api/v1/feeds/#{feed.id}", params: { feed: { name: "New Name" } }, as: :json + expect(response).to have_http_status(:unauthorized) + end +end From 066d2be0d91ea95cf90e171f30b697756907cfe1 Mon Sep 17 00:00:00 2001 From: Felipe Lima Date: Sat, 24 Oct 2026 09:17:57 -0300 Subject: [PATCH 11/14] =?UTF-8?q?feat:=20adiciona=20inativa=C3=A7=C3=A3o?= =?UTF-8?q?=20de=20ra=C3=A7=C3=B5es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/services/feeds/deactivate.rb | 11 +++++++++++ spec/services/feeds/deactivate_spec.rb | 24 ++++++++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 app/services/feeds/deactivate.rb create mode 100644 spec/services/feeds/deactivate_spec.rb diff --git a/app/services/feeds/deactivate.rb b/app/services/feeds/deactivate.rb new file mode 100644 index 0000000..02b6198 --- /dev/null +++ b/app/services/feeds/deactivate.rb @@ -0,0 +1,11 @@ +module Feeds + class Deactivate + class << self + def call(id:) + feed = Feeds::Find.call(id:) + feed.soft_delete! + feed + end + end + end +end diff --git a/spec/services/feeds/deactivate_spec.rb b/spec/services/feeds/deactivate_spec.rb new file mode 100644 index 0000000..07831e3 --- /dev/null +++ b/spec/services/feeds/deactivate_spec.rb @@ -0,0 +1,24 @@ +require 'rails_helper' + +RSpec.describe Feeds::Deactivate, type: :service do + describe '.call' do + it 'deactivates the feed' do + feed = create(:feed) + result = described_class.call(id: feed.id) + expect(result.reload).to be_deleted + end + + it 'raises an error when the feed is already deleted' do + feed = create(:feed, :deleted) + expect { + described_class.call(id: feed.id) + }.to raise_error(ActiveRecord::RecordNotFound) + end + + it 'raises an error when the feed does not exist' do + expect { + described_class.call(id: SecureRandom.uuid) + }.to raise_error(ActiveRecord::RecordNotFound) + end + end +end From 0ed4c45416b2f9560d9648ee9a6902d10a8863a7 Mon Sep 17 00:00:00 2001 From: Felipe Lima Date: Sat, 24 Oct 2026 09:27:19 -0300 Subject: [PATCH 12/14] =?UTF-8?q?feat:=20adiciona=20endpoint=20de=20inativ?= =?UTF-8?q?a=C3=A7=C3=A3o=20de=20ra=C3=A7=C3=B5es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controllers/api/v1/feeds_controller.rb | 5 +++ config/routes.rb | 2 +- spec/requests/api/v1/feeds/destroy_spec.rb | 36 ++++++++++++++++++++++ 3 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 spec/requests/api/v1/feeds/destroy_spec.rb diff --git a/app/controllers/api/v1/feeds_controller.rb b/app/controllers/api/v1/feeds_controller.rb index a63c0f9..13bf07d 100644 --- a/app/controllers/api/v1/feeds_controller.rb +++ b/app/controllers/api/v1/feeds_controller.rb @@ -27,6 +27,11 @@ def update render json: feed, serializer: FeedSerializer end + def destroy + Feeds::Deactivate.call(id: params[:id]) + head :no_content + end + private def feed_params diff --git a/config/routes.rb b/config/routes.rb index 75eb3ef..e3f14e4 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -27,7 +27,7 @@ resources :occupations, only: [ :create ] - resources :feeds, only: [ :create, :index, :show, :update ] + resources :feeds, only: [ :create, :index, :show, :update, :destroy ] get "health", to: "health#show" end diff --git a/spec/requests/api/v1/feeds/destroy_spec.rb b/spec/requests/api/v1/feeds/destroy_spec.rb new file mode 100644 index 0000000..ed49e66 --- /dev/null +++ b/spec/requests/api/v1/feeds/destroy_spec.rb @@ -0,0 +1,36 @@ +require 'rails_helper' + +RSpec.describe "DELETE /api/v1/feeds/:id", type: :request do + let (:user) { create(:user) } + let (:tokens) { Auth::CreateSession.call(user:) } + + let(:headers) do + { + "Authorization" => "Bearer #{tokens[:access_token]}" + } + end + + it "soft deletes the feed" do + feed = create(:feed) + delete "/api/v1/feeds/#{feed.id}", headers: headers + expect(response).to have_http_status(:no_content) + expect(feed.reload).to be_deleted + end + + it "returns not found when the feed does not exist" do + delete "/api/v1/feeds/#{SecureRandom.uuid}", headers: headers + expect(response).to have_http_status(:not_found) + end + + it "returns not found when the feed is already deleted" do + feed = create(:feed, :deleted) + delete "/api/v1/feeds/#{feed.id}", headers: headers + expect(response).to have_http_status(:not_found) + end + + it "returns unauthorized when no token is provided" do + feed = create(:feed) + delete "/api/v1/feeds/#{feed.id}" + expect(response).to have_http_status(:unauthorized) + end +end From 720de3f1d927d49d9d368735019eb47f44fa36cf Mon Sep 17 00:00:00 2001 From: Felipe Lima Date: Sat, 24 Oct 2026 09:36:59 -0300 Subject: [PATCH 13/14] =?UTF-8?q?chore(i18n):=20adiciona=20tradu=C3=A7?= =?UTF-8?q?=C3=B5es=20do=20modelo=20de=20ra=C3=A7=C3=A3o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/locales/en.yml | 6 ++++++ config/locales/pt-BR.yml | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/config/locales/en.yml b/config/locales/en.yml index 014a79d..5939c38 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -28,6 +28,9 @@ en: terrain: one: "Terrain" other: "Terrains" + feed: + one: "Feed" + other: "Feeds" attributes: user: name: "Name" @@ -41,3 +44,6 @@ en: name: "Name" rest_days: "Rest Days" status: "Status" + feed: + name: "Name" + stock_quantity: "Stock Quantity" diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml index ce76340..bf65159 100644 --- a/config/locales/pt-BR.yml +++ b/config/locales/pt-BR.yml @@ -33,6 +33,9 @@ pt-BR: terrain: one: "Terreno" other: "Terrenos" + feed: + one: "Ração" + other: "Rações" attributes: user: name: "Nome" @@ -46,3 +49,6 @@ pt-BR: name: "Nome" rest_days: "Dias de Descanso" status: "Status" + feed: + name: "Nome" + stock_quantity: "Quantidade em Estoque" From a4ff1529475745601b775fd5c97c37aded9a7b3c Mon Sep 17 00:00:00 2001 From: Felipe Lima Date: Sat, 24 Oct 2026 09:46:05 -0300 Subject: [PATCH 14/14] =?UTF-8?q?fix:=20atualiza=20depend=C3=AAncia=20vuln?= =?UTF-8?q?er=C3=A1vel=20crass?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Gemfile.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 0f92f19..08926e8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -97,7 +97,7 @@ GEM activesupport concurrent-ruby (1.3.8) connection_pool (3.0.2) - crass (1.0.7) + crass (1.0.8) cronex (0.15.0) tzinfo unicode (>= 0.4.4.5) @@ -417,7 +417,7 @@ CHECKSUMS case_transform (0.2) sha256=e2ad4418dceeb227cf474cc332cd5004c95c136c04186c1cceaad8ab8de6fe3b concurrent-ruby (1.3.8) sha256=b2f1be836e968ccc78ccfce277ea79c72a88633f22306782c16ff23fb415d1e1 connection_pool (3.0.2) sha256=33fff5ba71a12d2aa26cb72b1db8bba2a1a01823559fb01d29eb74c286e62e0a - crass (1.0.7) sha256=94868719948664c89ddcaf0a37c65048413dfcb1c869470a5f7a7ceb5390b295 + crass (1.0.8) sha256=7a0145c7a2c59f8e7923b3a69ce819c444823a00a5127d805298fea13be24efe cronex (0.15.0) sha256=21c794e085fad2951c4f2e279f440340a35ba2297e0b738f22f263f69fbe2186 date (3.5.1) sha256=750d06384d7b9c15d562c76291407d89e368dda4d4fff957eb94962d325a0dc0 debug (1.11.1) sha256=2e0b0ac6119f2207a6f8ac7d4a73ca8eb4e440f64da0a3136c30343146e952b6