Skip to content

Commit

Permalink
[AF-40] Fix immutability ROM objects (#41)
Browse files Browse the repository at this point in the history
* Removing default values from relations

- Update factories and test data

* Add automated tests for auction relations

* Update CHANGELOG and version
  • Loading branch information
ricardopacheco authored Apr 22, 2024
1 parent 1c49017 commit 448b604
Show file tree
Hide file tree
Showing 38 changed files with 621 additions and 121 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
## [Unreleased]

## [0.8.7] - 2024-04-23

### Added

- Removing default values from relations;
- Rebuild factories to not contain default values;
- Add automated tests for auction relations;

## [0.8.6] - 2024-04-23

### Added
Expand Down
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
auction_fun_core (0.8.6)
auction_fun_core (0.8.7)
activesupport (= 7.1.3.2)
bcrypt (= 3.1.20)
dotenv (= 3.1.0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ def self.call(attributes, &block)
end

def call(attributes)
values = yield validate(attributes)
values = yield validate_contract(attributes)
values = yield assign_default_values(values)

auction_repository.transaction do |_t|
@auction = yield persist(values)
Expand All @@ -38,14 +39,23 @@ def call(attributes)
# of the informed attributes.
# @param attrs [Hash] auction attributes
# @return [Dry::Monads::Result::Success, Dry::Monads::Result::Failure]
def validate(attrs)
def validate_contract(attrs)
contract = create_contract.call(attrs)

return Failure(contract.errors.to_h) if contract.failure?

Success(contract.to_h)
end

# By default, the auction status is set to 'scheduled'.
# @todo Refactor this method in the future to consider the status as of the auction start date.
# @param attrs [Hash] auction attributes
# @return [Dry::Monads::Result::Success]
def assign_default_values(attrs)
attrs[:status] = "scheduled"
Success(attrs)
end

# Calls the auction repository class to persist the attributes in the database.
# @param result [Hash] Auction validated attributes
# @return [ROM::Struct::Auction]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ def load_closed_auction_winners_and_participants(auction_id)
end

def update_finished_auction(auction, summary)
attrs = {kind: auction.kind, status: "finished"}
attrs = {status: "finished"}
attrs[:winner_id] = summary.winner_id if summary.winner_id.present?

Success(attrs)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ def load_penny_auction_winners_and_participants(auction_id)
end

def update_finished_auction(auction, summary)
attrs = {kind: auction.kind, status: "finished"}
attrs = {status: "finished"}
attrs[:winner_id] = summary.winner_id if summary.winner_id.present?

Success(attrs)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ def load_standard_auction_winners_and_participants(auction_id)
end

def update_finished_auction(auction, summary)
attrs = {kind: auction.kind, status: "finished"}
attrs = {status: "finished"}
attrs[:winner_id] = summary.winner_id if summary.winner_id.present?

Success(attrs)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ def self.call(attributes, &block)
# @todo Add custom doc
def call(attributes)
values = yield validate_contract(attributes)
values = yield assign_default_values(values)
values_with_encrypt_password = yield encrypt_password(values)

staff_repository.transaction do |_t|
Expand All @@ -44,6 +45,15 @@ def validate_contract(attrs)
Success(contract.to_h)
end

# By default, there can only be a single root staff. All other members have their type set to 'common'.
# @param attrs [Hash] user attributes
# @return [Dry::Monads::Result::Success]
def assign_default_values(attrs)
attrs[:kind] = "common"

Success(attrs)
end

# Transforms the password attribute, encrypting it to be saved in the database.
# @param result [Hash] Staff valid contract attributes
# @return [Hash] Valid staff database
Expand Down
135 changes: 64 additions & 71 deletions lib/auction_fun_core/relations/auctions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,8 @@ module Relations
class Auctions < ROM::Relation[:sql]
use :pagination, per_page: 10

KINDS = Types::Coercible::String.default("standard").enum("standard", "penny", "closed")
STATUSES = Types::Coercible::String.default("scheduled")
.enum("scheduled", "running", "paused", "canceled", "finished")
KINDS = Types::Coercible::String.enum("standard", "penny", "closed")
STATUSES = Types::Coercible::String.enum("scheduled", "running", "paused", "canceled", "finished")

schema(:auctions, infer: true) do
attribute :id, Types::Integer
Expand Down Expand Up @@ -42,15 +41,73 @@ class Auctions < ROM::Relation[:sql]
auto_struct(true)

def all(page = 1, per_page = 10, options = {bidders_count: 3})
raise "Invalid argument" unless page.is_a?(Integer) && per_page.is_a?(Integer)

offset = ((page - 1) * per_page)

read(all_auctions_with_bid_info(per_page, offset, options))
read("
SELECT a.id, a.title, a.description, a.kind, a.status, a.started_at, a.finished_at, a.stopwatch, a.initial_bid_cents,
(SELECT COUNT(*) FROM (SELECT * FROM bids WHERE bids.auction_id = a.id) dt) AS total_bids,
CASE
WHEN a.kind = 'standard' THEN
json_build_object(
'current', a.minimal_bid_cents,
'minimal', a.minimal_bid_cents,
'bidders', COALESCE(
json_agg(json_build_object('id', bi.id, 'user_id', users.id, 'name', users.name, 'value', bi.value_cents, 'date', bi.created_at) ORDER BY value_cents DESC)
FILTER (where bi.id IS NOT NULL AND users.id IS NOT NULL), '[]'::json
)
)
WHEN a.kind = 'penny' THEN
json_build_object(
'value', a.initial_bid_cents,
'bidders', COALESCE(
json_agg(json_build_object('id', bi.id, 'user_id', users.id, 'name', users.name, 'value', bi.value_cents, 'date', bi.created_at) ORDER BY value_cents DESC)
FILTER (where bi.id IS NOT NULL AND users.id IS NOT NULL), '[]'::json
)
)
WHEN a.kind = 'closed' THEN
json_build_object('minimal', (a.initial_bid_cents + (a.initial_bid_cents * 0.10))::int)
END as bids
FROM auctions as a
LEFT JOIN LATERAL (SELECT * FROM bids WHERE auction_id = a.id ORDER BY value_cents DESC LIMIT #{options[:bidders_count]}) as bi ON a.id = bi.auction_id
LEFT JOIN users ON bi.user_id = users.id AND bi.auction_id = a.id
GROUP BY a.id
LIMIT #{per_page} OFFSET #{offset}")
end

def info(auction_id, options = {bidders_count: 3})
raise "Invalid argument" unless auction_id.is_a?(Integer)

read(auction_with_bid_info(auction_id, options))
read("
SELECT a.id, a.title, a.description, a.kind, a.status, a.started_at, a.finished_at, a.stopwatch, a.initial_bid_cents,
(SELECT COUNT(*) FROM (SELECT * FROM bids WHERE bids.auction_id = #{auction_id}) dt) AS total_bids,
CASE
WHEN a.kind = 'standard' THEN
json_build_object(
'current', a.minimal_bid_cents,
'minimal', a.minimal_bid_cents,
'bidders', COALESCE(
json_agg(json_build_object('id', bi.id, 'user_id', users.id, 'name', users.name, 'value', bi.value_cents, 'date', bi.created_at) ORDER BY value_cents DESC)
FILTER (where bi.id IS NOT NULL AND users.id IS NOT NULL), '[]'::json
)
)
WHEN a.kind = 'penny' THEN
json_build_object(
'value', a.initial_bid_cents,
'bidders', COALESCE(
json_agg(json_build_object('id', bi.id, 'user_id', users.id, 'name', users.name, 'value', bi.value_cents, 'date', bi.created_at) ORDER BY value_cents DESC)
FILTER (where bi.id IS NOT NULL AND users.id IS NOT NULL), '[]'::json
)
)
WHEN a.kind = 'closed' THEN
json_build_object('minimal', (a.initial_bid_cents + (a.initial_bid_cents * 0.10))::int)
END as bids
FROM auctions as a
LEFT JOIN LATERAL (SELECT * FROM bids WHERE auction_id = a.id ORDER BY value_cents DESC LIMIT #{options[:bidders_count]}) as bi ON a.id = bi.auction_id AND a.id = #{auction_id}
LEFT JOIN users ON bi.user_id = users.id AND bi.auction_id = a.id
WHERE a.id = #{auction_id}
GROUP BY a.id")
end

# Retrieves the standard auction winner and other participating bidders for a specified auction.
Expand Down Expand Up @@ -152,7 +209,7 @@ def load_closed_auction_winners_and_participants(auction_id)
end

def load_winner_statistics(auction_id, winner_id)
raise "Invalid argument" unless auction_id.is_a?(Integer) || winner_id.is_a?(Integer)
raise "Invalid argument" unless auction_id.is_a?(Integer) && winner_id.is_a?(Integer)

read("SELECT a.id, COUNT(b.id) AS auction_total_bids, MAX(b.value_cents) AS winner_bid,
date(a.finished_at) as auction_date,
Expand All @@ -176,7 +233,7 @@ def load_winner_statistics(auction_id, winner_id)
end

def load_participant_statistics(auction_id, participant_id)
raise "Invalid argument" unless auction_id.is_a?(Integer) || participant_id.is_a?(Integer)
raise "Invalid argument" unless auction_id.is_a?(Integer) && participant_id.is_a?(Integer)

read("SELECT a.id, COUNT(b.id) AS auction_total_bids, MAX(b.value_cents) AS winner_bid,
date(a.finished_at) as auction_date,
Expand All @@ -198,70 +255,6 @@ def load_participant_statistics(auction_id, participant_id)
WHERE a.id = #{auction_id}
GROUP BY a.id")
end

private

def auction_with_bid_info(auction_id, options = {bidders_count: 3})
"SELECT a.id, a.title, a.description, a.kind, a.status, a.started_at, a.finished_at, a.stopwatch, a.initial_bid_cents,
(SELECT COUNT(*) FROM (SELECT * FROM bids WHERE bids.auction_id = #{auction_id}) dt) AS total_bids,
CASE
WHEN a.kind = 'standard' THEN
json_build_object(
'current', a.minimal_bid_cents,
'minimal', a.minimal_bid_cents,
'bidders', COALESCE(
json_agg(json_build_object('id', bi.id, 'user_id', users.id, 'name', users.name, 'value', bi.value_cents, 'date', bi.created_at) ORDER BY value_cents DESC)
FILTER (where bi.id IS NOT NULL AND users.id IS NOT NULL), '[]'::json
)
)
WHEN a.kind = 'penny' THEN
json_build_object(
'value', a.initial_bid_cents,
'bidders', COALESCE(
json_agg(json_build_object('id', bi.id, 'user_id', users.id, 'name', users.name, 'value', bi.value_cents, 'date', bi.created_at) ORDER BY value_cents DESC)
FILTER (where bi.id IS NOT NULL AND users.id IS NOT NULL), '[]'::json
)
)
WHEN a.kind = 'closed' THEN
json_build_object('minimal', (a.initial_bid_cents + (a.initial_bid_cents * 0.10))::int)
END as bids
FROM auctions as a
LEFT JOIN LATERAL (SELECT * FROM bids WHERE auction_id = a.id ORDER BY value_cents DESC LIMIT #{options[:bidders_count]}) as bi ON a.id = bi.auction_id AND a.id = #{auction_id}
LEFT JOIN users ON bi.user_id = users.id AND bi.auction_id = a.id
WHERE a.id = #{auction_id}
GROUP BY a.id"
end

def all_auctions_with_bid_info(per_page, offset, options = {bidders_count: 3})
"SELECT a.id, a.title, a.description, a.kind, a.status, a.started_at, a.finished_at, a.stopwatch, a.initial_bid_cents,
(SELECT COUNT(*) FROM (SELECT * FROM bids WHERE bids.auction_id = a.id) dt) AS total_bids,
CASE
WHEN a.kind = 'standard' THEN
json_build_object(
'current', a.minimal_bid_cents,
'minimal', a.minimal_bid_cents,
'bidders', COALESCE(
json_agg(json_build_object('id', bi.id, 'user_id', users.id, 'name', users.name, 'value', bi.value_cents, 'date', bi.created_at) ORDER BY value_cents DESC)
FILTER (where bi.id IS NOT NULL AND users.id IS NOT NULL), '[]'::json
)
)
WHEN a.kind = 'penny' THEN
json_build_object(
'value', a.initial_bid_cents,
'bidders', COALESCE(
json_agg(json_build_object('id', bi.id, 'user_id', users.id, 'name', users.name, 'value', bi.value_cents, 'date', bi.created_at) ORDER BY value_cents DESC)
FILTER (where bi.id IS NOT NULL AND users.id IS NOT NULL), '[]'::json
)
)
WHEN a.kind = 'closed' THEN
json_build_object('minimal', (a.initial_bid_cents + (a.initial_bid_cents * 0.10))::int)
END as bids
FROM auctions as a
LEFT JOIN LATERAL (SELECT * FROM bids WHERE auction_id = a.id ORDER BY value_cents DESC LIMIT #{options[:bidders_count]}) as bi ON a.id = bi.auction_id
LEFT JOIN users ON bi.user_id = users.id AND bi.auction_id = a.id
GROUP BY a.id
LIMIT #{per_page} OFFSET #{offset}"
end
end
end
end
2 changes: 1 addition & 1 deletion lib/auction_fun_core/relations/staffs.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ module Relations
class Staffs < ROM::Relation[:sql]
use :pagination, per_page: 10

STAFF_KINDS = Types::Coercible::String.default("common").enum("root", "common")
STAFF_KINDS = Types::Coercible::String.enum("root", "common")

schema(:staffs, infer: true) do
attribute :id, Types::Integer
Expand Down
2 changes: 1 addition & 1 deletion lib/auction_fun_core/version.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

module AuctionFunCore
VERSION = "0.8.6"
VERSION = "0.8.7"

# Required class module is a gem dependency
class Version; end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@
end

it_behaves_like "validate_stopwatch_contract" do
let(:auction) { Factory.structs[:auction, kind: :penny, initial_bid_cents: 1000] }
let(:auction) { Factory.structs[:auction, :with_kind_penny, :with_status_scheduled, initial_bid_cents: 1000] }
end
end
end
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

RSpec.describe AuctionFunCore::Contracts::AuctionContext::PostAuction::ParticipantContract, type: :contract do
let(:auction) { Factory[:auction, :default_standard, :with_winner] }
let(:auction) { Factory[:auction, :default_finished_standard, :with_winner] }
let(:participant) { Factory[:user] }

describe "#call" do
Expand Down Expand Up @@ -64,7 +64,7 @@
end

before do
Factory[:bid, auction_id: auction.id, user_id: participant.id, value_cents: auction.minimal_bid_cents]
Factory[:bid, auction: auction, user_id: participant.id, value_cents: auction.minimal_bid_cents]
end

it "expect return sucess" do
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

RSpec.describe AuctionFunCore::Contracts::AuctionContext::PostAuction::WinnerContract, type: :contract do
let(:auction) { Factory[:auction, :default_standard, :with_winner] }
let(:auction) { Factory[:auction, :default_finished_standard, :with_winner] }
let(:winner) { auction.winner }

describe "#call" do
Expand Down Expand Up @@ -45,7 +45,7 @@
context "when the informed winner is different from the one set in the auction" do
let(:real_winner) { Factory[:user] }
let(:fake_winner) { Factory[:user] }
let(:auction) { Factory[:auction, :default_standard, winner_id: real_winner.id] }
let(:auction) { Factory[:auction, :default_scheduled_standard, winner_id: real_winner.id] }
let(:attributes) do
{
auction_id: auction.id,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

RSpec.describe AuctionFunCore::Contracts::AuctionContext::Processor::PauseContract, type: :contract do
let(:auction) { Factory[:auction, :default_standard] }
let(:auction) { Factory[:auction, :default_scheduled_standard] }

describe "#call" do
subject(:contract) { described_class.new.call(attributes) }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

RSpec.describe AuctionFunCore::Contracts::AuctionContext::Processor::StartContract, type: :contract do
let(:auction) { Factory[:auction, :default_standard] }
let(:auction) { Factory[:auction, :default_scheduled_standard] }
let(:kinds) { described_class::AUCTION_KINDS.join(", ") }

describe "#call" do
Expand All @@ -18,7 +18,7 @@
end

it_behaves_like "validate_stopwatch_contract" do
let(:auction) { Factory[:auction, :default_penny] }
let(:auction) { Factory[:auction, :default_scheduled_penny] }
end

context "when auction_id is not founded on database" do
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

RSpec.describe AuctionFunCore::Contracts::AuctionContext::Processor::UnpauseContract, type: :contract do
let(:auction) { Factory[:auction, :default_standard] }
let(:auction) { Factory[:auction, :default_scheduled_standard] }

describe "#call" do
subject(:contract) { described_class.new.call(attributes) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@
end

context 'when auction kind is different of "closed"' do
let(:auction) { Factory[:auction, :default_penny] }
let(:auction) { Factory[:auction, :default_scheduled_penny] }
let(:attributes) { {auction_id: auction.id} }

it "expect failure with error messages" do
Expand Down
Loading

0 comments on commit 448b604

Please sign in to comment.