Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
42 changes: 42 additions & 0 deletions app/controllers/api/v1/feeds_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
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

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
end

def update
feed = Feeds::Update.call(id: params[:id], name: feed_params[:name])
render json: feed, serializer: FeedSerializer
end

def destroy
Feeds::Deactivate.call(id: params[:id])
head :no_content
end

private

def feed_params
params.require(:feed).permit(:name, :initial_stock_quantity)
end
end
end
end
15 changes: 15 additions & 0 deletions app/models/feed.rb
Original file line number Diff line number Diff line change
@@ -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
24 changes: 24 additions & 0 deletions app/queries/feeds/query.rb
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions app/serializers/feed_serializer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class FeedSerializer < ActiveModel::Serializer
attributes :id, :name, :stock_quantity, :created_at, :updated_at
end
9 changes: 9 additions & 0 deletions app/services/feeds/create.rb
Original file line number Diff line number Diff line change
@@ -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
11 changes: 11 additions & 0 deletions app/services/feeds/deactivate.rb
Original file line number Diff line number Diff line change
@@ -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
9 changes: 9 additions & 0 deletions app/services/feeds/find.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module Feeds
class Find
class << self
def call(id:)
Feed.active.find(id)
end
end
end
end
14 changes: 14 additions & 0 deletions app/services/feeds/list.rb
Original file line number Diff line number Diff line change
@@ -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
11 changes: 11 additions & 0 deletions app/services/feeds/update.rb
Original file line number Diff line number Diff line change
@@ -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
6 changes: 6 additions & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ en:
terrain:
one: "Terrain"
other: "Terrains"
feed:
one: "Feed"
other: "Feeds"
attributes:
user:
name: "Name"
Expand All @@ -41,3 +44,6 @@ en:
name: "Name"
rest_days: "Rest Days"
status: "Status"
feed:
name: "Name"
stock_quantity: "Stock Quantity"
6 changes: 6 additions & 0 deletions config/locales/pt-BR.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ pt-BR:
terrain:
one: "Terreno"
other: "Terrenos"
feed:
one: "Ração"
other: "Rações"
attributes:
user:
name: "Nome"
Expand All @@ -46,3 +49,6 @@ pt-BR:
name: "Nome"
rest_days: "Dias de Descanso"
status: "Status"
feed:
name: "Nome"
stock_quantity: "Quantidade em Estoque"
2 changes: 2 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@

resources :occupations, only: [ :create ]

resources :feeds, only: [ :create, :index, :show, :update, :destroy ]

get "health", to: "health#show"
end
end
Expand Down
13 changes: 13 additions & 0 deletions db/migrate/20261024090253_create_feeds.rb
Original file line number Diff line number Diff line change
@@ -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
11 changes: 10 additions & 1 deletion db/schema.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions spec/factories/feeds.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
FactoryBot.define do
factory :feed do
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
61 changes: 61 additions & 0 deletions spec/models/feed_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
require 'rails_helper'

RSpec.describe Feed, type: :model do
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
35 changes: 35 additions & 0 deletions spec/requests/api/v1/feeds/create_spec.rb
Original file line number Diff line number Diff line change
@@ -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
36 changes: 36 additions & 0 deletions spec/requests/api/v1/feeds/destroy_spec.rb
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading