Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[AF-25] Create Bids #26

Merged
merged 3 commits into from
Mar 6, 2024
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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
## [Unreleased]

## [0.8.0] - 2024-03-06

### Added

- Bid entity and your context to handle bid creation business rules.
- Internal Processor module in bids to handle specific types.

### Fixed

- auction migration name fix.

## [0.7.0] - 2024-02-29

### 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.7.0)
auction_fun_core (0.8.0)

GEM
remote: https://rubygems.org/
Expand Down
19 changes: 19 additions & 0 deletions db/migrate/20240304144422_create_bids.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# frozen_string_literal: true

ROM::SQL.migration do
change do
create_table :bids do
primary_key :id
foreign_key :user_id, :users, null: false
foreign_key :auction_id, :auctions, null: false

column :value_cents, Integer, null: false, default: 0
column :value_currency, String, null: false, default: "USD"

column :created_at, DateTime, null: false
column :updated_at, DateTime, null: false
end

add_index :bids, %i[auction_id user_id], name: "idx_user_auction"
end
end
19 changes: 19 additions & 0 deletions lib/auction_fun_core/commands/bid_context/create_bid.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# frozen_string_literal: true

module AuctionFunCore
module Commands
module BidContext
##
# Abstract base class for insert new tuples on bids table.
# @abstract
class CreateBid < ROM::Commands::Create[:sql]
relation :bids
register_as :create
result :one

use :timestamps
timestamp :created_at, :updated_at
end
end
end
end
15 changes: 15 additions & 0 deletions lib/auction_fun_core/commands/bid_context/delete_bid.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# frozen_string_literal: true

module AuctionFunCore
module Commands
module BidContext
##
# Abstract base class for removes tuples in bids table.
# @abstract
class DeleteBid < ROM::Commands::Delete[:sql]
relation :bids
register_as :delete
end
end
end
end
18 changes: 18 additions & 0 deletions lib/auction_fun_core/commands/bid_context/update_bid.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# frozen_string_literal: true

module AuctionFunCore
module Commands
module BidContext
##
# Abstract base class for updates all tuples in its bids table with new attributes
# @abstract
class UpdateBid < ROM::Commands::Update[:sql]
relation :bids
register_as :update

use :timestamps
timestamp :updated_at
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# frozen_string_literal: true

module AuctionFunCore
module Contracts
module BidContext
# Contract class to create new bids.
class CreateBidClosedContract < Contracts::ApplicationContract
option :user_repository, default: proc { Repos::UserContext::UserRepository.new }
option :auction_repository, default: proc { Repos::AuctionContext::AuctionRepository.new }
option :bid_repository, default: proc { Repos::BidContext::BidRepository.new }

# @param [Hash] opts Sets an allowed list of parameters, as well as some initial validations.
params do
required(:auction_id).filled(:integer)
required(:user_id).filled(:integer)
required(:value_cents).filled(:integer)

# Keys with a blank value are discarded.
before(:value_coercer) do |result|
result.to_h.compact
end
end

# Validation for auction.
# validate whether the given auction is valid at the database level.
# validate if the auction is open to receive bids
rule(:auction_id) do |context:|
context[:auction] ||= auction_repository.by_id(value)

if context[:auction]
if context[:auction].kind != "closed"
key.failure(I18n.t("contracts.errors.custom.bids.invalid_kind", kind: "closed"))
end

unless %w[scheduled running].include?(context[:auction].status)
key.failure(
I18n.t("contracts.errors.custom.bids.invalid_status", status: context[:auction].status)
)
end
else
key.failure(I18n.t("contracts.errors.custom.not_found"))
end
end

# Validation for user.
# Validate whether the given user is valid at the database level.
# Validates if user has already placed a bid
rule(:user_id) do |context:|
context[:user] ||= user_repository.by_id(value)

if context[:user]
if bid_repository.exists?(auction_id: values[:auction_id], user_id: value)
key.failure(I18n.t("contracts.errors.custom.bids.already_bidded"))
end
else
key.failure(I18n.t("contracts.errors.custom.not_found"))
end
end

# Validation for value bid.
# The bid amount must be greater than or equal to the starting bid.
rule(:value_cents) do |context:|
unless rule_error?(:user_id)
closed_auction_bid_value_is_gteq_initial_bid?(key, value, context[:auction].initial_bid_cents)
end
end

private

# Checks if bid amount must be greater than or equal to the starting bid.
def closed_auction_bid_value_is_gteq_initial_bid?(key, value_cents, minimal_bid_cents)
return unless value_cents < minimal_bid_cents

key.failure(I18n.t("contracts.errors.gteq?", num: Money.new(minimal_bid_cents).to_f))
end
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# frozen_string_literal: true

module AuctionFunCore
module Contracts
module BidContext
# Contract class to create new bids.
class CreateBidPennyContract < Contracts::ApplicationContract
option :user_repo, default: proc { Repos::UserContext::UserRepository.new }
option :auction_repo, default: proc { Repos::AuctionContext::AuctionRepository.new }

# @param [Hash] opts Sets an allowed list of parameters, as well as some initial validations.
params do
required(:auction_id).filled(:integer)
required(:user_id).filled(:integer)

# Keys with a blank value are discarded.
before(:value_coercer) do |result|
result.to_h.compact
end
end

# Validation for auction.
# validate whether the given auction is valid at the database level.
# validate if the auction is open to receive bids
rule(:auction_id) do |context:|
context[:auction] ||= auction_repo.by_id(value)

if context[:auction]
if context[:auction].kind != "penny"
key.failure(I18n.t("contracts.errors.custom.bids.invalid_kind", kind: "penny"))
end

unless %w[scheduled running].include?(context[:auction].status)
key.failure(
I18n.t("contracts.errors.custom.bids.invalid_status", status: context[:auction].status)
)
end
else
key.failure(I18n.t("contracts.errors.custom.not_found"))
end
end

# Validation for user.
# Validate whether the given user is valid at the database level.
# Validates if user has enough balance to bid.
rule(:user_id) do |context:|
context[:user] ||= user_repo.by_id(value)

if context[:user]
unless rule_error?(:auction_id)
penny_auction_check_user_has_balance?(
key, context[:auction].initial_bid_cents, context[:user].balance_cents
)
end
else
key.failure(I18n.t("contracts.errors.custom.not_found"))
end
end

private

# Checks if user has enough balance to bid.
def penny_auction_check_user_has_balance?(key, auction_bid_cents, balance_cents)
key.failure(I18n.t("contracts.errors.custom.bids.insufficient_balance")) if balance_cents < auction_bid_cents
end
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# frozen_string_literal: true

module AuctionFunCore
module Contracts
module BidContext
# Contract class to create new bids.
class CreateBidStandardContract < Contracts::ApplicationContract
option :user_repo, default: proc { Repos::UserContext::UserRepository.new }
option :auction_repo, default: proc { Repos::AuctionContext::AuctionRepository.new }

# @param [Hash] opts Sets an allowed list of parameters, as well as some initial validations.
params do
required(:auction_id).filled(:integer)
required(:user_id).filled(:integer)
required(:value_cents).filled(:integer)

# Keys with a blank value are discarded.
before(:value_coercer) do |result|
result.to_h.compact
end
end

# Validation for auction.
# validate whether the given auction is valid at the database level.
# validate if the auction is open to receive bids
rule(:auction_id) do |context:|
context[:auction] ||= auction_repo.by_id(value)

if context[:auction]
if context[:auction].kind != "standard"
key.failure(I18n.t("contracts.errors.custom.bids.invalid_kind", kind: "standard"))
end

unless %w[scheduled running].include?(context[:auction].status)
key.failure(
I18n.t("contracts.errors.custom.bids.invalid_status", status: context[:auction].status)
)
end
else
key.failure(I18n.t("contracts.errors.custom.not_found"))
end
end

# Validation for user.
# Validate whether the given user is valid at the database level.
rule(:user_id) do |context:|
context[:user] ||= user_repo.by_id(value)
key.failure(I18n.t("contracts.errors.custom.not_found")) unless context[:user]
end

# Validation for value bid.
# must be greater than or equal to the auction's minimal bid.
rule(:value_cents) do |context:|
standard_auction_valid_bid?(key, value, context[:auction].minimal_bid_cents)
end

private

# Checks if the bid amount is greather than or equal to minimum bid.
def standard_auction_valid_bid?(key, value_cents, minimal_bid_cents)
return if value_cents >= minimal_bid_cents

key.failure(I18n.t("contracts.errors.gteq?", num: Money.new(minimal_bid_cents).to_f))
end
end
end
end
end
10 changes: 10 additions & 0 deletions lib/auction_fun_core/entities/bid.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# frozen_string_literal: true

module AuctionFunCore
module Entities
# Bid Relations class. This return simple objects with attribute readers
# to represent data in your bid.
class Bid < ROM::Struct
end
end
end
2 changes: 2 additions & 0 deletions lib/auction_fun_core/events/app.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ class App
register_event("auctions.started")
register_event("auctions.finished")

register_event("bids.created")

register_event("staffs.authentication")
register_event("staffs.registration")

Expand Down
6 changes: 6 additions & 0 deletions lib/auction_fun_core/events/listener.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ def on_auctions_finished(auction)
logger("Finished auction: #{auction.to_h}")
end

# Listener for to *bids.created* event.
# @param event [Integer] Auction ID
def on_bids_created(bid)
logger("Create bid with: #{bid.to_h}")
end

# Listener for to *staffs.authentication* event.
# @param attributes [Hash] Authentication attributes
# @option staff_id [Integer] Staff ID
Expand Down
Loading
Loading