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
48 changes: 48 additions & 0 deletions app/controllers/api/v1/feed_movements_controller.rb
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions app/models/feed.rb
Original file line number Diff line number Diff line change
@@ -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 }

Expand Down
14 changes: 14 additions & 0 deletions app/models/feed_movement.rb
Original file line number Diff line number Diff line change
@@ -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
12 changes: 12 additions & 0 deletions app/serializers/feed_movement_serializer.rb
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions app/serializers/feed_summary_serializer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class FeedSummarySerializer < ActiveModel::Serializer
attributes :id, :name, :stock_quantity
end
23 changes: 23 additions & 0 deletions app/services/feed_movements/create_entry.rb
Original file line number Diff line number Diff line change
@@ -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
38 changes: 38 additions & 0 deletions app/services/feed_movements/create_exit.rb
Original file line number Diff line number Diff line change
@@ -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
15 changes: 15 additions & 0 deletions app/services/feed_movements/history.rb
Original file line number Diff line number Diff line change
@@ -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
12 changes: 12 additions & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -31,6 +33,9 @@ en:
feed:
one: "Feed"
other: "Feeds"
feed_movement:
one: "Feed Movement"
other: "Feed Movements"
attributes:
user:
name: "Name"
Expand All @@ -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"
12 changes: 12 additions & 0 deletions config/locales/pt-BR.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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"
Expand All @@ -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"
6 changes: 5 additions & 1 deletion config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 17 additions & 0 deletions db/migrate/20261024125351_create_feed_movements.rb
Original file line number Diff line number Diff line change
@@ -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
17 changes: 16 additions & 1 deletion db/schema.rb

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

21 changes: 21 additions & 0 deletions spec/factories/feed_movements.rb
Original file line number Diff line number Diff line change
@@ -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
64 changes: 64 additions & 0 deletions spec/models/feed_movement_spec.rb
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading