] an array of user IDs of the other participants, excluding the winner
+ def load_closed_auction_winners_and_participants(auction_id)
+ raise "Invalid argument" unless auction_id.is_a?(Integer)
+
+ read("SELECT a.id, a.kind, a.status, w.user_id AS winner_id, COALESCE(COUNT(b.id), 0) AS total_bids,
+ COALESCE(
+ ARRAY_REMOVE(ARRAY_AGG(DISTINCT b.user_id ORDER BY b.user_id), w.user_id), ARRAY[]::INT[]
+ ) AS participant_ids
+ FROM auctions a
+ LEFT JOIN bids b ON a.id = b.auction_id
+ LEFT JOIN (
+ SELECT auction_id, user_id, MAX(value_cents) AS value_cents
+ FROM bids
+ WHERE auction_id = #{auction_id}
+ GROUP BY auction_id, user_id
+ ORDER BY value_cents DESC
+ LIMIT 1
+ ) AS w ON a.id = w.auction_id
+ WHERE a.id = #{auction_id}
+ GROUP BY a.id, w.user_id")
+ end
+
+ def load_winner_statistics(auction_id, winner_id)
+ 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,
+ (SELECT COUNT(*) FROM bids b2
+ WHERE b2.auction_id = #{auction_id}
+ AND b2.user_id = #{winner_id}
+ ) AS winner_total_bids
+ FROM auctions a
+ LEFT JOIN bids b ON a.id = b.auction_id AND a.id = #{auction_id}
+ LEFT JOIN users u ON u.id = b.user_id AND u.id = #{winner_id}
+ LEFT JOIN (
+ SELECT auction_id, user_id, MAX(value_cents) AS value_cents
+ FROM bids
+ WHERE auction_id = #{auction_id}
+ GROUP BY auction_id, user_id
+ ORDER BY value_cents DESC
+ LIMIT 1
+ ) AS w ON a.id = w.auction_id
+ WHERE a.id = #{auction_id}
+ GROUP BY a.id")
+ end
+
+ def load_participant_statistics(auction_id, participant_id)
+ 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,
+ (SELECT COUNT(*) FROM bids b2
+ WHERE b2.auction_id = #{auction_id}
+ AND b2.user_id = #{participant_id}
+ ) AS winner_total_bids
+ FROM auctions a
+ LEFT JOIN bids b ON a.id = b.auction_id AND a.id = #{auction_id}
+ LEFT JOIN users u ON u.id = b.user_id AND u.id = #{participant_id}
+ LEFT JOIN (
+ SELECT auction_id, user_id, MAX(value_cents) AS value_cents
+ FROM bids
+ WHERE auction_id = #{auction_id}
+ GROUP BY auction_id, user_id
+ ORDER BY value_cents DESC
+ LIMIT 1
+ ) AS w ON a.id = w.auction_id
+ WHERE a.id = #{auction_id}
+ GROUP BY a.id")
+ end
+
private
def auction_with_bid_info(auction_id, options = {bidders_count: 3})
diff --git a/lib/auction_fun_core/services/mail/auction_context/post_auction/participant_mailer.rb b/lib/auction_fun_core/services/mail/auction_context/post_auction/participant_mailer.rb
new file mode 100644
index 0000000..b83243f
--- /dev/null
+++ b/lib/auction_fun_core/services/mail/auction_context/post_auction/participant_mailer.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module AuctionFunCore
+ module Services
+ module Mail
+ module AuctionContext
+ module PostAuction
+ class ParticipantMailer
+ include IdleMailer::Mailer
+ include IdleMailer::TemplateManager
+
+ # @param auction [ROM::Struct::Auction] The auction object
+ # @param participant [ROM::Struct::User] The user object
+ # @param statistics [OpenStruct] Statistics object
+ def initialize(auction, participant, statistics)
+ @auction = auction
+ @participant = participant
+ @statistics = statistics
+ mail.to = participant.email
+ mail.subject = I18n.t("mail.auction_context.post_auction.participant_mailer.subject", title: @auction.title)
+ end
+
+ def self.template_name
+ IdleMailer.config.templates.join("auction_context/post_auction/participant")
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/auction_fun_core/services/mail/auction_context/post_auction/winner_mailer.rb b/lib/auction_fun_core/services/mail/auction_context/post_auction/winner_mailer.rb
new file mode 100644
index 0000000..83dd8bb
--- /dev/null
+++ b/lib/auction_fun_core/services/mail/auction_context/post_auction/winner_mailer.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module AuctionFunCore
+ module Services
+ module Mail
+ module AuctionContext
+ module PostAuction
+ class WinnerMailer
+ include IdleMailer::Mailer
+ include IdleMailer::TemplateManager
+
+ # @param auction [ROM::Struct::Auction] The auction object
+ # @param winner [ROM::Struct::User] The user object
+ # @param statistics [OpenStruct] Statistics object
+ def initialize(auction, winner, statistics)
+ @auction = auction
+ @winner = winner
+ @statistics = statistics
+ mail.to = winner.email
+ mail.subject = I18n.t("mail.auction_context.post_auction.winner_mailer.subject", title: @auction.title)
+ end
+
+ def self.template_name
+ IdleMailer.config.templates.join("auction_context/post_auction/winner")
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/auction_fun_core/services/mail/templates/auction_context/post_auction/participant.html.erb b/lib/auction_fun_core/services/mail/templates/auction_context/post_auction/participant.html.erb
new file mode 100644
index 0000000..6a2eee3
--- /dev/null
+++ b/lib/auction_fun_core/services/mail/templates/auction_context/post_auction/participant.html.erb
@@ -0,0 +1,173 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+
+
+ |
+
+
+
+
+
+ <%= I18n.t("mail.general.hello", name: @participant.name) %>,
+
+
+ |
+
+
+
+
+ |
+
+
+ |
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <%= raw I18n.t("mail.auction_context.post_auction.participant_mailer.body.description", title: @auction.title) %>
+
+ |
+
+
+
+
+
+ - <%= I18n.t("mail.auction_context.post_auction.participant_mailer.body.stats.auction_total_bids", auction_total_bids: @statistics.auction_total_bids) %>
+ - <%= I18n.t("mail.auction_context.post_auction.participant_mailer.body.stats.winner_bid", winner_bid: @statistics.winner_bid) %>
+ - <%= I18n.t("mail.auction_context.post_auction.participant_mailer.body.stats.auction_date") %>: <%= I18n.l(@statistics.auction_date, format: :default) %>
+
+ |
+
+
+
+
+ <%= I18n.t("application.general.team") %>
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+
+
+ |
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+
+
+ |
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+
+
+ |
+
+
+
+ |
+
+
+ |
+
+
+
+
+ |
+
+
+ |
+
+
+
+
+ |
+
+
diff --git a/lib/auction_fun_core/services/mail/templates/auction_context/post_auction/winner.html.erb b/lib/auction_fun_core/services/mail/templates/auction_context/post_auction/winner.html.erb
new file mode 100644
index 0000000..1226fc7
--- /dev/null
+++ b/lib/auction_fun_core/services/mail/templates/auction_context/post_auction/winner.html.erb
@@ -0,0 +1,174 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+
+
+ |
+
+
+
+
+
+ <%= I18n.t("mail.general.hello", name: @winner.name) %>,
+
+
+ |
+
+
+
+
+ |
+
+
+ |
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <%= raw I18n.t("mail.auction_context.post_auction.winner_mailer.body.description", title: @auction.title) %>
+
+ |
+
+
+
+
+
+ - <%= raw I18n.t("mail.auction_context.post_auction.winner_mailer.body.item.title", title: @auction.title) %>
+ - <%= I18n.t("mail.auction_context.post_auction.winner_mailer.body.item.winner_bid", winner_bid: @statistics.winner_bid) %>
+ - <%= I18n.t("mail.auction_context.post_auction.winner_mailer.body.item.auction_total_bids", auction_total_bids: @statistics.auction_total_bids) %>
+ - <%= I18n.t("mail.auction_context.post_auction.winner_mailer.body.item.auction_date") %>: <%= I18n.l(@statistics.auction_date, format: :default) %>
+
+ |
+
+
+
+
+ <%= I18n.t("application.general.team") %>
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+
+
+ |
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+
+
+ |
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+
+
+ |
+
+
+
+ |
+
+
+ |
+
+
+
+
+ |
+
+
+ |
+
+
+
+
+ |
+
+
diff --git a/lib/auction_fun_core/services/mail/templates/user_context/registration.html.erb b/lib/auction_fun_core/services/mail/templates/user_context/registration.html.erb
index 6585517..e060031 100644
--- a/lib/auction_fun_core/services/mail/templates/user_context/registration.html.erb
+++ b/lib/auction_fun_core/services/mail/templates/user_context/registration.html.erb
@@ -29,7 +29,7 @@
- <%= I18n.t("mail.general.hello", name: @user.name) %>,
+ <%= I18n.t("application.general.hello", name: @user.name) %>,
|
@@ -79,7 +79,7 @@
- <%= I18n.t("mail.general.team") %>
+ <%= I18n.t("application.general.team") %>
|
diff --git a/lib/auction_fun_core/version.rb b/lib/auction_fun_core/version.rb
index 88c0395..a2580a2 100644
--- a/lib/auction_fun_core/version.rb
+++ b/lib/auction_fun_core/version.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module AuctionFunCore
- VERSION = "0.8.5"
+ VERSION = "0.8.6"
# Required class module is a gem dependency
class Version; end
diff --git a/lib/auction_fun_core/workers/application_job.rb b/lib/auction_fun_core/workers/application_job.rb
index 58da564..1192e57 100644
--- a/lib/auction_fun_core/workers/application_job.rb
+++ b/lib/auction_fun_core/workers/application_job.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true
+require "sidekiq"
+
module AuctionFunCore
module Workers
# Abstract base class for background jobs.
diff --git a/lib/auction_fun_core/workers/operations/auction_context/post_auction/participant_operation_job.rb b/lib/auction_fun_core/workers/operations/auction_context/post_auction/participant_operation_job.rb
new file mode 100644
index 0000000..4847927
--- /dev/null
+++ b/lib/auction_fun_core/workers/operations/auction_context/post_auction/participant_operation_job.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module AuctionFunCore
+ module Workers
+ module Operations
+ module AuctionContext
+ module PostAuction
+ ##
+ # BackgroundJob class for call finish auction operation.
+ class ParticipantOperationJob < Workers::ApplicationJob
+ include Import["repos.user_context.user_repository"]
+ include Import["repos.auction_context.auction_repository"]
+ include Import["operations.auction_context.post_auction.participant_operation"]
+
+ # @todo Add detailed documentation
+ def perform(auction_id, participant_id, retry_count = 0)
+ auction = auction_repository.by_id!(auction_id)
+ participant = user_repository.by_id!(participant_id)
+
+ participant_operation.call(auction_id: auction.id, participant_id: participant.id)
+ rescue => e
+ capture_exception(e, {auction_id: auction_id, participant_id: participant_id, retry_count: retry_count})
+ raise if retry_count >= MAX_RETRIES
+
+ interval = backoff_exponential_job(retry_count)
+ self.class.perform_at(interval, auction_id, participant_id, retry_count + 1)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/auction_fun_core/workers/operations/auction_context/post_auction/winner_operation_job.rb b/lib/auction_fun_core/workers/operations/auction_context/post_auction/winner_operation_job.rb
new file mode 100644
index 0000000..12b61a8
--- /dev/null
+++ b/lib/auction_fun_core/workers/operations/auction_context/post_auction/winner_operation_job.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module AuctionFunCore
+ module Workers
+ module Operations
+ module AuctionContext
+ module PostAuction
+ ##
+ # BackgroundJob class for call finish auction operation.
+ class WinnerOperationJob < Workers::ApplicationJob
+ include Import["repos.user_context.user_repository"]
+ include Import["repos.auction_context.auction_repository"]
+ include Import["operations.auction_context.post_auction.winner_operation"]
+
+ # @todo Add detailed documentation
+ def perform(auction_id, winner_id, retry_count = 0)
+ auction = auction_repository.by_id!(auction_id)
+ winner = user_repository.by_id!(winner_id)
+
+ winner_operation.call(auction_id: auction.id, winner_id: winner.id)
+ rescue => e
+ capture_exception(e, {auction_id: auction_id, winner_id: winner_id, retry_count: retry_count})
+ raise if retry_count >= MAX_RETRIES
+
+ interval = backoff_exponential_job(retry_count)
+ self.class.perform_at(interval, auction_id, winner_id, retry_count + 1)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/auction_fun_core/workers/operations/auction_context/processor/finish/closed_operation_job.rb b/lib/auction_fun_core/workers/operations/auction_context/processor/finish/closed_operation_job.rb
new file mode 100644
index 0000000..f7c905b
--- /dev/null
+++ b/lib/auction_fun_core/workers/operations/auction_context/processor/finish/closed_operation_job.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module AuctionFunCore
+ module Workers
+ module Operations
+ module AuctionContext
+ module Processor
+ module Finish
+ ##
+ # BackgroundJob class for call finish closed auction operation.
+ #
+ class ClosedOperationJob < Workers::ApplicationJob
+ include Import["repos.auction_context.auction_repository"]
+ include Import["operations.auction_context.processor.finish.closed_operation"]
+
+ # @todo Add detailed documentation
+ def perform(auction_id, retry_count = 0)
+ auction = auction_repository.by_id!(auction_id)
+
+ closed_operation.call(auction_id: auction.id)
+ rescue => e
+ capture_exception(e, {auction_id: auction_id, retry_count: retry_count})
+ raise if retry_count >= MAX_RETRIES
+
+ interval = backoff_exponential_job(retry_count)
+ self.class.perform_at(interval, auction_id, retry_count + 1)
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/auction_fun_core/workers/operations/auction_context/processor/finish/penny_operation_job.rb b/lib/auction_fun_core/workers/operations/auction_context/processor/finish/penny_operation_job.rb
new file mode 100644
index 0000000..d7ae8a0
--- /dev/null
+++ b/lib/auction_fun_core/workers/operations/auction_context/processor/finish/penny_operation_job.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module AuctionFunCore
+ module Workers
+ module Operations
+ module AuctionContext
+ module Processor
+ module Finish
+ ##
+ # BackgroundJob class for call finish penny auction operation.
+ #
+ class PennyOperationJob < Workers::ApplicationJob
+ include Sidekiq::Worker
+ include Import["repos.auction_context.auction_repository"]
+ include Import["operations.auction_context.processor.finish.penny_operation"]
+
+ sidekiq_options queue: "default", lock: :until_executed, on_conflict: :replace
+
+ # @todo Add detailed documentation
+ def perform(auction_id, retry_count = 0)
+ auction = auction_repository.by_id!(auction_id)
+
+ penny_operation.call(auction_id: auction.id)
+ rescue => e
+ capture_exception(e, {auction_id: auction_id, retry_count: retry_count})
+ raise if retry_count >= MAX_RETRIES
+
+ interval = backoff_exponential_job(retry_count)
+ self.class.perform_at(interval, auction_id, retry_count + 1)
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/auction_fun_core/workers/operations/auction_context/processor/finish/standard_operation_job.rb b/lib/auction_fun_core/workers/operations/auction_context/processor/finish/standard_operation_job.rb
new file mode 100644
index 0000000..1fd44cd
--- /dev/null
+++ b/lib/auction_fun_core/workers/operations/auction_context/processor/finish/standard_operation_job.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module AuctionFunCore
+ module Workers
+ module Operations
+ module AuctionContext
+ module Processor
+ module Finish
+ ##
+ # BackgroundJob class for call finish standard auction operation.
+ #
+ class StandardOperationJob < Workers::ApplicationJob
+ include Import["repos.auction_context.auction_repository"]
+ include Import["operations.auction_context.processor.finish.standard_operation"]
+
+ # @todo Add detailed documentation
+ def perform(auction_id, retry_count = 0)
+ auction = auction_repository.by_id!(auction_id)
+
+ standard_operation.call(auction_id: auction.id)
+ rescue => e
+ capture_exception(e, {auction_id: auction_id, retry_count: retry_count})
+ raise if retry_count >= MAX_RETRIES
+
+ interval = backoff_exponential_job(retry_count)
+ self.class.perform_at(interval, auction_id, retry_count + 1)
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/auction_fun_core/workers/operations/auction_context/processor/finish_operation_job.rb b/lib/auction_fun_core/workers/operations/auction_context/processor/finish_operation_job.rb
deleted file mode 100644
index c42f62f..0000000
--- a/lib/auction_fun_core/workers/operations/auction_context/processor/finish_operation_job.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-# frozen_string_literal: true
-
-module AuctionFunCore
- module Workers
- module Operations
- module AuctionContext
- module Processor
- ##
- # BackgroundJob class for call finish auction operation.
- #
- class FinishOperationJob < Workers::ApplicationJob
- include Import["repos.auction_context.auction_repository"]
- include Import["operations.auction_context.processor.finish_operation"]
-
- # @todo Add detailed documentation
- def perform(auction_id, retry_count = 0)
- auction = auction_repository.by_id!(auction_id)
-
- finish_operation.call(auction.id)
- rescue => e
- capture_exception(e, {auction_id: auction_id, retry_count: retry_count})
- raise if retry_count >= MAX_RETRIES
-
- interval = backoff_exponential_job(retry_count)
- self.class.perform_at(interval, auction_id, retry_count + 1)
- end
- end
- end
- end
- end
- end
-end
diff --git a/lib/auction_fun_core/workers/services/mail/auction_context/post_auction/participant_mailer_job.rb b/lib/auction_fun_core/workers/services/mail/auction_context/post_auction/participant_mailer_job.rb
new file mode 100644
index 0000000..5a24fcd
--- /dev/null
+++ b/lib/auction_fun_core/workers/services/mail/auction_context/post_auction/participant_mailer_job.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+module AuctionFunCore
+ module Workers
+ module Services
+ module Mail
+ module AuctionContext
+ module PostAuction
+ ##
+ # Background job class responsible for adding emails to the queue.
+ #
+ class ParticipantMailerJob < AuctionFunCore::Workers::ApplicationJob
+ include Import["repos.user_context.user_repository"]
+ include Import["repos.auction_context.auction_repository"]
+
+ # @param auction_id [Integer] auction ID
+ # @param participant_id [Integer] user ID
+ def perform(auction_id, participant_id, retry_count = 0)
+ auction = auction_repository.by_id!(auction_id)
+ participant = user_repository.by_id!(participant_id)
+
+ statistics = relation.load_participant_statistics.call(auction.id, participant.id).first
+
+ participant_mailer.new(auction, participant, statistics).deliver
+ rescue => e
+ capture_exception(e, {auction_id: auction_id, participant_id: participant_id, retry_count: retry_count})
+ raise e if retry_count >= MAX_RETRIES
+
+ interval = backoff_exponential_job(retry_count)
+ self.class.perform_at(interval, auction_id, participant_id, retry_count + 1)
+ end
+
+ private
+
+ # Since the shipping code structure does not follow project conventions,
+ # making the default injection dependency would be more complicated.
+ # Therefore, here I directly explain the class to be called.
+ def participant_mailer
+ AuctionFunCore::Services::Mail::AuctionContext::PostAuction::ParticipantMailer
+ end
+
+ def relation
+ AuctionFunCore::Application[:container].relations[:auctions]
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/auction_fun_core/workers/services/mail/auction_context/post_auction/winner_mailer_job.rb b/lib/auction_fun_core/workers/services/mail/auction_context/post_auction/winner_mailer_job.rb
new file mode 100644
index 0000000..47e1865
--- /dev/null
+++ b/lib/auction_fun_core/workers/services/mail/auction_context/post_auction/winner_mailer_job.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+module AuctionFunCore
+ module Workers
+ module Services
+ module Mail
+ module AuctionContext
+ module PostAuction
+ ##
+ # Background job class responsible for adding emails to the queue.
+ #
+ class WinnerMailerJob < AuctionFunCore::Workers::ApplicationJob
+ include Import["repos.user_context.user_repository"]
+ include Import["repos.auction_context.auction_repository"]
+
+ # @param auction_id [Integer] auction ID
+ # @param winner_id [Integer] user ID
+ def perform(auction_id, winner_id, retry_count = 0)
+ auction = auction_repository.by_id!(auction_id)
+ winner = user_repository.by_id!(winner_id)
+
+ statistics = relation.load_winner_statistics.call(auction_id, winner_id).first
+
+ winner_mailer.new(auction, winner, statistics).deliver
+ rescue => e
+ capture_exception(e, {auction_id: auction_id, winner_id: winner_id, retry_count: retry_count})
+ raise e if retry_count >= MAX_RETRIES
+
+ interval = backoff_exponential_job(retry_count)
+ self.class.perform_at(interval, auction_id, winner_id, retry_count + 1)
+ end
+
+ private
+
+ # Since the shipping code structure does not follow project conventions,
+ # making the default injection dependency would be more complicated.
+ # Therefore, here I directly explain the class to be called.
+ def winner_mailer
+ AuctionFunCore::Services::Mail::AuctionContext::PostAuction::WinnerMailer
+ end
+
+ def relation
+ AuctionFunCore::Application[:container].relations[:auctions]
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/auction_fun_core/contracts/auction_context/post_auction/participant_contract_spec.rb b/spec/auction_fun_core/contracts/auction_context/post_auction/participant_contract_spec.rb
new file mode 100644
index 0000000..efa0bb3
--- /dev/null
+++ b/spec/auction_fun_core/contracts/auction_context/post_auction/participant_contract_spec.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+RSpec.describe AuctionFunCore::Contracts::AuctionContext::PostAuction::ParticipantContract, type: :contract do
+ let(:auction) { Factory[:auction, :default_standard, :with_winner] }
+ let(:participant) { Factory[:user] }
+
+ describe "#call" do
+ subject(:contract) { described_class.new.call(attributes) }
+
+ context "when attributes are invalid" do
+ let(:attributes) { Dry::Core::Constants::EMPTY_HASH }
+
+ it "expect failure with error messages" do
+ expect(contract.errors[:auction_id]).to include(I18n.t("contracts.errors.key?"))
+ expect(contract.errors[:participant_id]).to include(I18n.t("contracts.errors.key?"))
+ end
+ end
+
+ context "when auction is not found on database" do
+ let(:attributes) do
+ {
+ auction_id: 2_234_231,
+ participant_id: participant.id
+ }
+ end
+
+ it "expect failure with error messages" do
+ expect(contract.errors[:auction_id]).to include(I18n.t("contracts.errors.custom.not_found"))
+ end
+ end
+
+ context "when participant is not found on database" do
+ let(:attributes) do
+ {
+ auction_id: auction.id,
+ participant_id: 2_234_231
+ }
+ end
+
+ it "expect failure with error messages" do
+ expect(contract.errors[:participant_id]).to include(I18n.t("contracts.errors.custom.not_found"))
+ end
+ end
+
+ context "when the user did not bid in the auction" do
+ let(:attributes) do
+ {
+ auction_id: auction.id,
+ participant_id: participant.id
+ }
+ end
+
+ it "expect failure with error messages" do
+ expect(contract.errors[:participant_id]).to include(I18n.t("none", scope: described_class::I18N_SCOPE))
+ end
+ end
+
+ context "when the user placed at least one bid in the auction" do
+ let(:attributes) do
+ {
+ auction_id: auction.id,
+ participant_id: participant.id
+ }
+ end
+
+ before do
+ Factory[:bid, auction_id: auction.id, user_id: participant.id, value_cents: auction.minimal_bid_cents]
+ end
+
+ it "expect return sucess" do
+ expect(contract).to be_success
+ expect(contract.context[:auction]).to be_a(AuctionFunCore::Entities::Auction)
+ expect(contract.context[:participant]).to be_a(AuctionFunCore::Entities::User)
+ end
+ end
+ end
+end
diff --git a/spec/auction_fun_core/contracts/auction_context/post_auction/winner_contract_spec.rb b/spec/auction_fun_core/contracts/auction_context/post_auction/winner_contract_spec.rb
new file mode 100644
index 0000000..63e9026
--- /dev/null
+++ b/spec/auction_fun_core/contracts/auction_context/post_auction/winner_contract_spec.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+
+RSpec.describe AuctionFunCore::Contracts::AuctionContext::PostAuction::WinnerContract, type: :contract do
+ let(:auction) { Factory[:auction, :default_standard, :with_winner] }
+ let(:winner) { auction.winner }
+
+ describe "#call" do
+ subject(:contract) { described_class.new.call(attributes) }
+
+ context "when attributes are invalid" do
+ let(:attributes) { Dry::Core::Constants::EMPTY_HASH }
+
+ it "expect failure with error messages" do
+ expect(contract.errors[:auction_id]).to include(I18n.t("contracts.errors.key?"))
+ expect(contract.errors[:winner_id]).to include(I18n.t("contracts.errors.key?"))
+ end
+ end
+
+ context "when auction is not found on database" do
+ let(:attributes) do
+ {
+ auction_id: 2_234_231,
+ winner_id: winner.id
+ }
+ end
+
+ it "expect failure with error messages" do
+ expect(contract.errors[:auction_id]).to include(I18n.t("contracts.errors.custom.not_found"))
+ end
+ end
+
+ context "when winner is not found on database" do
+ let(:attributes) do
+ {
+ auction_id: auction.id,
+ winner_id: 2_234_231
+ }
+ end
+
+ it "expect failure with error messages" do
+ expect(contract.errors[:winner_id]).to include(I18n.t("contracts.errors.custom.not_found"))
+ end
+ end
+
+ 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(:attributes) do
+ {
+ auction_id: auction.id,
+ winner_id: fake_winner.id
+ }
+ end
+
+ it "expect failure with error messages" do
+ expect(contract.errors[:winner_id]).to include(I18n.t("wrong", scope: described_class::I18N_SCOPE))
+ end
+ end
+
+ context "when the informed winner is the same as the one set in the auction" do
+ let(:attributes) do
+ {
+ auction_id: auction.id,
+ winner_id: winner.id
+ }
+ end
+
+ it "expect return sucess" do
+ expect(contract).to be_success
+ expect(contract.context[:auction]).to be_a(AuctionFunCore::Entities::Auction)
+ expect(contract.context[:winner]).to be_a(AuctionFunCore::Entities::User)
+ end
+ end
+ end
+end
diff --git a/spec/auction_fun_core/contracts/auction_context/processor/finish/closed_contract_spec.rb b/spec/auction_fun_core/contracts/auction_context/processor/finish/closed_contract_spec.rb
new file mode 100644
index 0000000..eb12149
--- /dev/null
+++ b/spec/auction_fun_core/contracts/auction_context/processor/finish/closed_contract_spec.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+RSpec.describe AuctionFunCore::Contracts::AuctionContext::Processor::Finish::ClosedContract, type: :contract do
+ describe "#call" do
+ subject(:contract) { described_class.new.call(attributes) }
+
+ context "when attributes are invalid" do
+ let(:attributes) { Dry::Core::Constants::EMPTY_HASH }
+
+ it "expect failure with error messages" do
+ expect(contract).to be_failure
+ expect(contract.errors[:auction_id]).to include(I18n.t("contracts.errors.key?"))
+ end
+ end
+
+ context "when auction is not found on database" do
+ let(:attributes) { {auction_id: rand(10_000..1_000_000)} }
+
+ it "expect failure with error messages" do
+ expect(contract).to be_failure
+ expect(contract.errors[:auction_id]).to include(I18n.t("contracts.errors.custom.not_found"))
+ end
+ end
+
+ context "when auction kind is not equal to 'closed'" do
+ let(:auction) { Factory[:auction, :default_running_standard] }
+ let(:attributes) { {auction_id: auction.id} }
+
+ it "expect failure with error messages" do
+ expect(contract).to be_failure
+ expect(contract.errors[:base]).to include(
+ I18n.t("contracts.errors.custom.auction_context.processor.finish.invalid_kind")
+ )
+ end
+ end
+
+ context "when auction status is not equal to 'running'" do
+ let(:auction) { Factory[:auction, :default_paused_standard] }
+ let(:attributes) { {auction_id: auction.id} }
+
+ it "expect failure with error messages" do
+ expect(contract).to be_failure
+ expect(contract.errors[:base]).to include(
+ I18n.t("contracts.errors.custom.auction_context.processor.finish.invalid_status")
+ )
+ end
+ end
+
+ context "when attributes are valid" do
+ let(:auction) { Factory[:auction, :default_running_closed] }
+ let(:attributes) { {auction_id: auction.id} }
+
+ it "expect return success" do
+ expect(contract).to be_success
+ expect(contract.context[:auction]).to be_a(AuctionFunCore::Entities::Auction)
+ end
+ end
+ end
+end
diff --git a/spec/auction_fun_core/contracts/auction_context/processor/finish/penny_contract_spec.rb b/spec/auction_fun_core/contracts/auction_context/processor/finish/penny_contract_spec.rb
new file mode 100644
index 0000000..77b60f3
--- /dev/null
+++ b/spec/auction_fun_core/contracts/auction_context/processor/finish/penny_contract_spec.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+RSpec.describe AuctionFunCore::Contracts::AuctionContext::Processor::Finish::PennyContract, type: :contract do
+ describe "#call" do
+ subject(:contract) { described_class.new.call(attributes) }
+
+ context "when attributes are invalid" do
+ let(:attributes) { Dry::Core::Constants::EMPTY_HASH }
+
+ it "expect failure with error messages" do
+ expect(contract).to be_failure
+ expect(contract.errors[:auction_id]).to include(I18n.t("contracts.errors.key?"))
+ end
+ end
+
+ context "when auction is not found on database" do
+ let(:attributes) { {auction_id: rand(10_000..1_000_000)} }
+
+ it "expect failure with error messages" do
+ expect(contract).to be_failure
+ expect(contract.errors[:auction_id]).to include(I18n.t("contracts.errors.custom.not_found"))
+ end
+ end
+
+ context "when auction kind is not equal to 'penny'" do
+ let(:auction) { Factory[:auction, :default_running_standard] }
+ let(:attributes) { {auction_id: auction.id} }
+
+ it "expect failure with error messages" do
+ expect(contract).to be_failure
+ expect(contract.errors[:base]).to include(
+ I18n.t("contracts.errors.custom.auction_context.processor.finish.invalid_kind")
+ )
+ end
+ end
+
+ context "when auction status is not equal to 'running'" do
+ let(:auction) { Factory[:auction, :default_paused_standard] }
+ let(:attributes) { {auction_id: auction.id} }
+
+ it "expect failure with error messages" do
+ expect(contract).to be_failure
+ expect(contract.errors[:base]).to include(
+ I18n.t("contracts.errors.custom.auction_context.processor.finish.invalid_status")
+ )
+ end
+ end
+
+ context "when attributes are valid" do
+ let(:auction) { Factory[:auction, :default_running_penny] }
+ let(:attributes) { {auction_id: auction.id} }
+
+ it "expect return success" do
+ expect(contract).to be_success
+ expect(contract.context[:auction]).to be_a(AuctionFunCore::Entities::Auction)
+ end
+ end
+ end
+end
diff --git a/spec/auction_fun_core/contracts/auction_context/processor/finish/standard_contract_spec.rb b/spec/auction_fun_core/contracts/auction_context/processor/finish/standard_contract_spec.rb
new file mode 100644
index 0000000..2752659
--- /dev/null
+++ b/spec/auction_fun_core/contracts/auction_context/processor/finish/standard_contract_spec.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+RSpec.describe AuctionFunCore::Contracts::AuctionContext::Processor::Finish::StandardContract, type: :contract do
+ describe "#call" do
+ subject(:contract) { described_class.new.call(attributes) }
+
+ context "when attributes are invalid" do
+ let(:attributes) { Dry::Core::Constants::EMPTY_HASH }
+
+ it "expect failure with error messages" do
+ expect(contract).to be_failure
+ expect(contract.errors[:auction_id]).to include(I18n.t("contracts.errors.key?"))
+ end
+ end
+
+ context "when auction is not found on database" do
+ let(:attributes) { {auction_id: rand(10_000..1_000_000)} }
+
+ it "expect failure with error messages" do
+ expect(contract).to be_failure
+ expect(contract.errors[:auction_id]).to include(I18n.t("contracts.errors.custom.not_found"))
+ end
+ end
+
+ context "when auction kind is not equal to 'standard'" do
+ let(:auction) { Factory[:auction, :default_running_penny] }
+ let(:attributes) { {auction_id: auction.id} }
+
+ it "expect failure with error messages" do
+ expect(contract).to be_failure
+ expect(contract.errors[:base]).to include(
+ I18n.t("contracts.errors.custom.auction_context.processor.finish.invalid_kind")
+ )
+ end
+ end
+
+ context "when auction status is not equal to 'running'" do
+ let(:auction) { Factory[:auction, :default_paused_standard] }
+ let(:attributes) { {auction_id: auction.id} }
+
+ it "expect failure with error messages" do
+ expect(contract).to be_failure
+ expect(contract.errors[:base]).to include(
+ I18n.t("contracts.errors.custom.auction_context.processor.finish.invalid_status")
+ )
+ end
+ end
+
+ context "when attributes are valid" do
+ let(:auction) { Factory[:auction, :default_running_standard] }
+ let(:attributes) { {auction_id: auction.id} }
+
+ it "expect return success" do
+ expect(contract).to be_success
+ expect(contract.context[:auction]).to be_a(AuctionFunCore::Entities::Auction)
+ end
+ end
+ end
+end
diff --git a/spec/auction_fun_core/contracts/auction_context/processor/finish_contract_spec.rb b/spec/auction_fun_core/contracts/auction_context/processor/finish_contract_spec.rb
deleted file mode 100644
index 4020923..0000000
--- a/spec/auction_fun_core/contracts/auction_context/processor/finish_contract_spec.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-# frozen_string_literal: true
-
-RSpec.describe AuctionFunCore::Contracts::AuctionContext::Processor::FinishContract, type: :contract do
- let(:auction) { Factory[:auction, :default_running_standard] }
-
- describe "#call" do
- subject(:contract) { described_class.new.call(attributes) }
-
- context "when attributes are invalid" do
- let(:attributes) { Dry::Core::Constants::EMPTY_HASH }
-
- it "expect failure with error messages" do
- expect(contract).to be_failure
- expect(contract.errors[:auction_id]).to include(I18n.t("contracts.errors.key?"))
- end
- end
-
- context "when attributes are valid" do
- let(:attributes) { {auction_id: auction.id} }
-
- it "expect return success" do
- expect(contract).to be_success
- expect(contract.context[:auction]).to be_a(AuctionFunCore::Entities::Auction)
- end
- end
- end
-end
diff --git a/spec/auction_fun_core/entities/auction_spec.rb b/spec/auction_fun_core/entities/auction_spec.rb
index 1e2f141..f8199b9 100644
--- a/spec/auction_fun_core/entities/auction_spec.rb
+++ b/spec/auction_fun_core/entities/auction_spec.rb
@@ -18,4 +18,22 @@
expect(auction.minimal_bid).to be_a_instance_of(Money)
end
end
+
+ describe "#winner?" do
+ context "when there is an associated FK" do
+ subject(:auction) { Factory.structs[:auction, :with_winner, winner_id: 1] }
+
+ it "expects to return true when it has a winning user associated." do
+ expect(auction.winner?).to be_truthy
+ end
+ end
+
+ context "when there is no associated FK" do
+ subject(:auction) { Factory.structs[:auction] }
+
+ it "expect return a user object" do
+ expect(auction.winner?).to be_falsey
+ end
+ end
+ end
end
diff --git a/spec/auction_fun_core/operations/auction_context/create_operation_spec.rb b/spec/auction_fun_core/operations/auction_context/create_operation_spec.rb
index 7685794..b077f87 100644
--- a/spec/auction_fun_core/operations/auction_context/create_operation_spec.rb
+++ b/spec/auction_fun_core/operations/auction_context/create_operation_spec.rb
@@ -80,18 +80,15 @@
.to receive(:perform_at)
end
- it "expect return success" do
- expect(operation).to be_success
- expect(operation.failure).to be_nil
- end
-
- it "expect create auction on database with correct status and dispatch event and processes" do
+ it "expect return success creating auction on database with correct status and dispatch event and processes" do
expect { operation }.to change(auction_repository, :count).from(0).to(1)
+ expect(operation).to be_success
expect(operation.success.status).to eq("scheduled")
expect(AuctionFunCore::Application[:event]).to have_received(:publish).once
expect(AuctionFunCore::Workers::Operations::AuctionContext::Processor::StartOperationJob)
- .to have_received(:perform_at).once
+ .to have_received(:perform_at)
+ .once
end
end
end
diff --git a/spec/auction_fun_core/operations/auction_context/post_auction/participant_operation_spec.rb b/spec/auction_fun_core/operations/auction_context/post_auction/participant_operation_spec.rb
new file mode 100644
index 0000000..77679bc
--- /dev/null
+++ b/spec/auction_fun_core/operations/auction_context/post_auction/participant_operation_spec.rb
@@ -0,0 +1,88 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe AuctionFunCore::Operations::AuctionContext::PostAuction::ParticipantOperation, type: :operation do
+ let(:auction_repository) { AuctionFunCore::Repos::AuctionContext::AuctionRepository.new }
+ let(:participant) { Factory[:user] }
+ let(:winner) { Factory[:user] }
+ let(:auction) { Factory[:auction, :default_finished_standard, winner_id: winner.id] }
+
+ describe ".call(auction_id, &block)" do
+ let(:operation) { described_class }
+
+ context "when block is given" do
+ context "when operation happens with success" do
+ let(:attributes) { {auction_id: auction.id, participant_id: participant.id} }
+
+ before do
+ Factory[:bid, user_id: winner.id, auction_id: auction.id, value_cents: auction.minimal_bid_cents * 2]
+ Factory[:bid, user_id: participant.id, auction_id: auction.id, value_cents: auction.minimal_bid_cents]
+ end
+
+ it "expect result success matching block" do
+ matched_success = nil
+ matched_failure = nil
+
+ operation.call(attributes) do |o|
+ o.success { |v| matched_success = v }
+ o.failure { |f| matched_failure = f }
+ end
+
+ expect(matched_success).to include(participant)
+ expect(matched_failure).to be_nil
+ end
+ end
+
+ context "when operation happens with failure" do
+ let(:attributes) { Dry::Core::Constants::EMPTY_HASH }
+
+ it "expect result matching block" do
+ matched_success = nil
+ matched_failure = nil
+
+ operation.call(attributes) do |o|
+ o.success { |v| matched_success = v }
+ o.failure { |f| matched_failure = f }
+ end
+
+ expect(matched_success).to be_nil
+ expect(matched_failure[:auction_id]).to include(I18n.t("contracts.errors.key?"))
+ expect(matched_failure[:participant_id]).to include(I18n.t("contracts.errors.key?"))
+ end
+ end
+ end
+ end
+
+ describe "#call" do
+ subject(:operation) { described_class.new.call(attributes) }
+
+ context "when contract is invalid" do
+ let(:attributes) { Dry::Core::Constants::EMPTY_HASH }
+
+ it "expect return failure with error messages" do
+ expect(operation.failure[:auction_id]).to include(I18n.t("contracts.errors.key?"))
+ expect(operation.failure[:participant_id]).to include(I18n.t("contracts.errors.key?"))
+ end
+ end
+
+ context "when contract is valid" do
+ let(:attributes) { {auction_id: auction.id, participant_id: participant.id} }
+
+ before do
+ Factory[:bid, user_id: winner.id, auction_id: auction.id, value_cents: auction.minimal_bid_cents * 2]
+ Factory[:bid, user_id: participant.id, auction_id: auction.id, value_cents: auction.minimal_bid_cents]
+ end
+
+ it "expect send winning email with auction statistics and payment instructions" do
+ allow(AuctionFunCore::Workers::Services::Mail::AuctionContext::PostAuction::ParticipantMailerJob)
+ .to receive(:perform_async).with(auction.id, participant.id)
+
+ operation
+
+ expect(AuctionFunCore::Workers::Services::Mail::AuctionContext::PostAuction::ParticipantMailerJob)
+ .to have_received(:perform_async).with(auction.id, participant.id).once
+ end
+ end
+ end
+end
diff --git a/spec/auction_fun_core/operations/auction_context/post_auction/winner_operation_spec.rb b/spec/auction_fun_core/operations/auction_context/post_auction/winner_operation_spec.rb
new file mode 100644
index 0000000..161e6e8
--- /dev/null
+++ b/spec/auction_fun_core/operations/auction_context/post_auction/winner_operation_spec.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe AuctionFunCore::Operations::AuctionContext::PostAuction::WinnerOperation, type: :operation do
+ let(:auction_repository) { AuctionFunCore::Repos::AuctionContext::AuctionRepository.new }
+ let(:winner) { Factory[:user] }
+ let(:auction) { Factory[:auction, :default_finished_standard, winner_id: winner.id] }
+
+ describe ".call(auction_id, &block)" do
+ let(:operation) { described_class }
+
+ context "when block is given" do
+ context "when operation happens with success" do
+ let(:attributes) { {auction_id: auction.id, winner_id: winner.id} }
+ it "expect result success matching block" do
+ matched_success = nil
+ matched_failure = nil
+
+ operation.call(attributes) do |o|
+ o.success { |v| matched_success = v }
+ o.failure { |f| matched_failure = f }
+ end
+
+ expect(matched_success).to include(winner)
+ expect(matched_failure).to be_nil
+ end
+ end
+
+ context "when operation happens with failure" do
+ let(:attributes) { Dry::Core::Constants::EMPTY_HASH }
+
+ it "expect result matching block" do
+ matched_success = nil
+ matched_failure = nil
+
+ operation.call(attributes) do |o|
+ o.success { |v| matched_success = v }
+ o.failure { |f| matched_failure = f }
+ end
+
+ expect(matched_success).to be_nil
+ expect(matched_failure[:auction_id]).to include(I18n.t("contracts.errors.key?"))
+ expect(matched_failure[:winner_id]).to include(I18n.t("contracts.errors.key?"))
+ end
+ end
+ end
+ end
+
+ describe "#call" do
+ subject(:operation) { described_class.new.call(attributes) }
+
+ context "when contract is invalid" do
+ let(:attributes) { Dry::Core::Constants::EMPTY_HASH }
+
+ it "expect return failure with error messages" do
+ expect(operation.failure[:auction_id]).to include(I18n.t("contracts.errors.key?"))
+ expect(operation.failure[:winner_id]).to include(I18n.t("contracts.errors.key?"))
+ end
+ end
+
+ context "when contract is valid" do
+ let(:attributes) { {auction_id: auction.id, winner_id: winner.id} }
+
+ it "expect send winning email with auction statistics and payment instructions" do
+ allow(AuctionFunCore::Workers::Services::Mail::AuctionContext::PostAuction::WinnerMailerJob)
+ .to receive(:perform_async).with(auction.id, winner.id)
+
+ operation
+
+ expect(AuctionFunCore::Workers::Services::Mail::AuctionContext::PostAuction::WinnerMailerJob)
+ .to have_received(:perform_async).with(auction.id, winner.id).once
+ end
+ end
+ end
+end
diff --git a/spec/auction_fun_core/operations/auction_context/processor/finish/closed_operation_spec.rb b/spec/auction_fun_core/operations/auction_context/processor/finish/closed_operation_spec.rb
new file mode 100644
index 0000000..2209551
--- /dev/null
+++ b/spec/auction_fun_core/operations/auction_context/processor/finish/closed_operation_spec.rb
@@ -0,0 +1,107 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe AuctionFunCore::Operations::AuctionContext::Processor::Finish::ClosedOperation, type: :operation do
+ let(:auction_repository) { AuctionFunCore::Repos::AuctionContext::AuctionRepository.new }
+ let(:auction) { Factory[:auction, :default_running_closed, finished_at: Time.current] }
+
+ describe ".call(auction_id, &block)" do
+ let(:operation) { described_class }
+
+ context "when block is given" do
+ context "when operation happens with success" do
+ let(:attributes) { {auction_id: auction.id} }
+ it "expect result success matching block" do
+ matched_success = nil
+ matched_failure = nil
+
+ operation.call(attributes) do |o|
+ o.success { |v| matched_success = v }
+ o.failure { |f| matched_failure = f }
+ end
+
+ expect(matched_success.id).to eq(auction.id)
+ expect(matched_success.status).to eq("finished")
+ expect(matched_failure).to be_nil
+ end
+ end
+
+ context "when operation happens with failure" do
+ let(:attributes) { Dry::Core::Constants::EMPTY_HASH }
+
+ it "expect result matching block" do
+ matched_success = nil
+ matched_failure = nil
+
+ operation.call(attributes) do |o|
+ o.success { |v| matched_success = v }
+ o.failure { |f| matched_failure = f }
+ end
+
+ expect(matched_success).to be_nil
+ expect(matched_failure[:auction_id]).to include(I18n.t("contracts.errors.key?"))
+ end
+ end
+ end
+ end
+
+ describe "#call" do
+ subject(:operation) { described_class.new.call(attributes) }
+
+ context "when contract is invalid" do
+ let(:attributes) { Dry::Core::Constants::EMPTY_HASH }
+
+ it "expect not change auction status" do
+ expect { operation }.not_to change { auction_repository.by_id(auction.id).status }
+ end
+
+ it "expect return failure with error messages" do
+ expect(operation.failure[:auction_id]).to include(I18n.t("contracts.errors.key?"))
+ end
+ end
+
+ context "when contract is valid" do
+ let(:attributes) { {auction_id: auction.id} }
+
+ it "expect update status auction record on database" do
+ expect { operation }
+ .to change { auction_repository.by_id(auction.id).status }
+ .from("running")
+ .to("finished")
+ end
+
+ it "expect publish the auction finish event" do
+ allow(AuctionFunCore::Application[:event]).to receive(:publish)
+
+ operation
+
+ expect(AuctionFunCore::Application[:event]).to have_received(:publish).once
+ end
+
+ context "when the auction has a winning bid with other participations" do
+ let(:participant) { Factory[:user] }
+ let(:winner) { Factory[:user] }
+
+ before do
+ Factory[:bid, auction: auction, user: winner, value_cents: (auction.minimal_bid_cents * 2)]
+ Factory[:bid, auction: auction, user: participant, value_cents: auction.minimal_bid_cents]
+ end
+
+ it "expect call winner operation and participant operation jobs" do
+ allow(AuctionFunCore::Workers::Operations::AuctionContext::PostAuction::WinnerOperationJob)
+ .to receive(:perform_async).with(auction.id, winner.id)
+ allow(AuctionFunCore::Workers::Operations::AuctionContext::PostAuction::ParticipantOperationJob)
+ .to receive(:perform_async).with(auction.id, participant.id)
+
+ operation
+
+ expect(AuctionFunCore::Workers::Operations::AuctionContext::PostAuction::WinnerOperationJob)
+ .to have_received(:perform_async).with(auction.id, winner.id).once
+ expect(AuctionFunCore::Workers::Operations::AuctionContext::PostAuction::ParticipantOperationJob)
+ .to have_received(:perform_async).with(auction.id, participant.id).once
+ end
+ end
+ end
+ end
+end
diff --git a/spec/auction_fun_core/operations/auction_context/processor/finish/penny_operation_spec.rb b/spec/auction_fun_core/operations/auction_context/processor/finish/penny_operation_spec.rb
new file mode 100644
index 0000000..c858ac8
--- /dev/null
+++ b/spec/auction_fun_core/operations/auction_context/processor/finish/penny_operation_spec.rb
@@ -0,0 +1,107 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe AuctionFunCore::Operations::AuctionContext::Processor::Finish::PennyOperation, type: :operation do
+ let(:auction_repository) { AuctionFunCore::Repos::AuctionContext::AuctionRepository.new }
+ let(:auction) { Factory[:auction, :default_running_penny, finished_at: Time.current] }
+
+ describe ".call(auction_id, &block)" do
+ let(:operation) { described_class }
+
+ context "when block is given" do
+ context "when operation happens with success" do
+ let(:attributes) { {auction_id: auction.id} }
+ it "expect result success matching block" do
+ matched_success = nil
+ matched_failure = nil
+
+ operation.call(attributes) do |o|
+ o.success { |v| matched_success = v }
+ o.failure { |f| matched_failure = f }
+ end
+
+ expect(matched_success.id).to eq(auction.id)
+ expect(matched_success.status).to eq("finished")
+ expect(matched_failure).to be_nil
+ end
+ end
+
+ context "when operation happens with failure" do
+ let(:attributes) { Dry::Core::Constants::EMPTY_HASH }
+
+ it "expect result matching block" do
+ matched_success = nil
+ matched_failure = nil
+
+ operation.call(attributes) do |o|
+ o.success { |v| matched_success = v }
+ o.failure { |f| matched_failure = f }
+ end
+
+ expect(matched_success).to be_nil
+ expect(matched_failure[:auction_id]).to include(I18n.t("contracts.errors.key?"))
+ end
+ end
+ end
+ end
+
+ describe "#call" do
+ subject(:operation) { described_class.new.call(attributes) }
+
+ context "when contract is invalid" do
+ let(:attributes) { Dry::Core::Constants::EMPTY_HASH }
+
+ it "expect not change auction status" do
+ expect { operation }.not_to change { auction_repository.by_id(auction.id).status }
+ end
+
+ it "expect return failure with error messages" do
+ expect(operation.failure[:auction_id]).to include(I18n.t("contracts.errors.key?"))
+ end
+ end
+
+ context "when contract is valid" do
+ let(:attributes) { {auction_id: auction.id} }
+
+ it "expect update status auction record on database" do
+ expect { operation }
+ .to change { auction_repository.by_id(auction.id).status }
+ .from("running")
+ .to("finished")
+ end
+
+ it "expect publish the auction finish event" do
+ allow(AuctionFunCore::Application[:event]).to receive(:publish)
+
+ operation
+
+ expect(AuctionFunCore::Application[:event]).to have_received(:publish).once
+ end
+
+ context "when the auction has a winning bid with other participations" do
+ let(:participant) { Factory[:user] }
+ let(:winner) { Factory[:user] }
+
+ before do
+ Factory[:bid, auction: auction, user: participant, value_cents: auction.minimal_bid_cents]
+ Factory[:bid, auction: auction, user: winner, value_cents: auction.minimal_bid_cents]
+ end
+
+ it "expect call winner operation and participant operation jobs" do
+ allow(AuctionFunCore::Workers::Operations::AuctionContext::PostAuction::WinnerOperationJob)
+ .to receive(:perform_async).with(auction.id, winner.id)
+ allow(AuctionFunCore::Workers::Operations::AuctionContext::PostAuction::ParticipantOperationJob)
+ .to receive(:perform_async).with(auction.id, participant.id)
+
+ operation
+
+ expect(AuctionFunCore::Workers::Operations::AuctionContext::PostAuction::WinnerOperationJob)
+ .to have_received(:perform_async).with(auction.id, winner.id).once
+ expect(AuctionFunCore::Workers::Operations::AuctionContext::PostAuction::ParticipantOperationJob)
+ .to have_received(:perform_async).with(auction.id, participant.id).once
+ end
+ end
+ end
+ end
+end
diff --git a/spec/auction_fun_core/operations/auction_context/processor/finish_operation_spec.rb b/spec/auction_fun_core/operations/auction_context/processor/finish/standard_operation_spec.rb
similarity index 58%
rename from spec/auction_fun_core/operations/auction_context/processor/finish_operation_spec.rb
rename to spec/auction_fun_core/operations/auction_context/processor/finish/standard_operation_spec.rb
index cb3d765..ea9743b 100644
--- a/spec/auction_fun_core/operations/auction_context/processor/finish_operation_spec.rb
+++ b/spec/auction_fun_core/operations/auction_context/processor/finish/standard_operation_spec.rb
@@ -2,7 +2,7 @@
require "spec_helper"
-RSpec.describe AuctionFunCore::Operations::AuctionContext::Processor::FinishOperation, type: :operation do
+RSpec.describe AuctionFunCore::Operations::AuctionContext::Processor::Finish::StandardOperation, type: :operation do
let(:auction_repository) { AuctionFunCore::Repos::AuctionContext::AuctionRepository.new }
let(:auction) { Factory[:auction, :default_running_standard, finished_at: Time.current] }
@@ -11,11 +11,12 @@
context "when block is given" do
context "when operation happens with success" do
+ let(:attributes) { {auction_id: auction.id} }
it "expect result success matching block" do
matched_success = nil
matched_failure = nil
- operation.call(auction.id) do |o|
+ operation.call(attributes) do |o|
o.success { |v| matched_success = v }
o.failure { |f| matched_failure = f }
end
@@ -27,41 +28,41 @@
end
context "when operation happens with failure" do
- let(:auction_id) { nil }
+ let(:attributes) { Dry::Core::Constants::EMPTY_HASH }
it "expect result matching block" do
matched_success = nil
matched_failure = nil
- operation.call(auction_id) do |o|
+ operation.call(attributes) do |o|
o.success { |v| matched_success = v }
o.failure { |f| matched_failure = f }
end
expect(matched_success).to be_nil
- expect(matched_failure[:auction_id]).to include(I18n.t("contracts.errors.filled?"))
+ expect(matched_failure[:auction_id]).to include(I18n.t("contracts.errors.key?"))
end
end
end
end
describe "#call" do
- subject(:operation) { described_class.new.call(auction_id) }
+ subject(:operation) { described_class.new.call(attributes) }
context "when contract is invalid" do
- let(:auction_id) { nil }
+ let(:attributes) { Dry::Core::Constants::EMPTY_HASH }
it "expect not change auction status" do
expect { operation }.not_to change { auction_repository.by_id(auction.id).status }
end
it "expect return failure with error messages" do
- expect(operation.failure[:auction_id]).to include(I18n.t("contracts.errors.filled?"))
+ expect(operation.failure[:auction_id]).to include(I18n.t("contracts.errors.key?"))
end
end
context "when contract is valid" do
- let(:auction_id) { auction.id }
+ let(:attributes) { {auction_id: auction.id} }
it "expect update status auction record on database" do
expect { operation }
@@ -77,6 +78,29 @@
expect(AuctionFunCore::Application[:event]).to have_received(:publish).once
end
+
+ context "when the auction has a winning bid with other participations" do
+ let(:winner) { Factory[:user] }
+
+ before do
+ Factory[:bid, auction: auction, value_cents: auction.minimal_bid_cents]
+ Factory[:bid, auction: auction, user: winner, value_cents: (auction.minimal_bid_cents * 2)]
+ end
+
+ it "expect call winner operation and participant operation jobs" do
+ allow(AuctionFunCore::Workers::Operations::AuctionContext::PostAuction::WinnerOperationJob)
+ .to receive(:perform_async)
+ allow(AuctionFunCore::Workers::Operations::AuctionContext::PostAuction::ParticipantOperationJob)
+ .to receive(:perform_async)
+
+ operation
+
+ expect(AuctionFunCore::Workers::Operations::AuctionContext::PostAuction::WinnerOperationJob)
+ .to have_received(:perform_async).once
+ expect(AuctionFunCore::Workers::Operations::AuctionContext::PostAuction::ParticipantOperationJob)
+ .to have_received(:perform_async).once
+ end
+ end
end
end
end
diff --git a/spec/auction_fun_core/operations/auction_context/processor/start_operation_spec.rb b/spec/auction_fun_core/operations/auction_context/processor/start_operation_spec.rb
index 18893f4..c933541 100644
--- a/spec/auction_fun_core/operations/auction_context/processor/start_operation_spec.rb
+++ b/spec/auction_fun_core/operations/auction_context/processor/start_operation_spec.rb
@@ -76,30 +76,41 @@
}
end
- it "expect update status auction record on database" do
+ it "expect update status auction record on database and publish the auction start event" do
+ allow(AuctionFunCore::Application[:event]).to receive(:publish)
+
expect { operation }.to change { auction_repository.by_id(auction.id).status }.from("scheduled").to("running")
+
+ expect(AuctionFunCore::Application[:event]).to have_received(:publish).once
end
- it "expect create a new job to finish the auction" do
- allow(AuctionFunCore::Workers::Operations::AuctionContext::Processor::FinishOperationJob).to receive(:perform_at)
+ context "when auction kind is equal to 'standard'" do
+ it "expect create a new job to finish the standard auction" do
+ allow(AuctionFunCore::Workers::Operations::AuctionContext::Processor::Finish::StandardOperationJob).to receive(:perform_at)
- operation
+ operation
- expect(AuctionFunCore::Workers::Operations::AuctionContext::Processor::FinishOperationJob)
- .to have_received(:perform_at)
- .with(auction.finished_at, auction.id)
- .once
+ expect(AuctionFunCore::Workers::Operations::AuctionContext::Processor::Finish::StandardOperationJob)
+ .to have_received(:perform_at)
+ .once
+ end
end
- it "expect publish the auction start event" do
- allow(AuctionFunCore::Application[:event]).to receive(:publish)
+ context "when auction kind is equal to 'closed'" do
+ let(:auction) { Factory[:auction, :default_closed, started_at: Time.current] }
- operation
+ it "expect create a new job to finish the closed auction" do
+ allow(AuctionFunCore::Workers::Operations::AuctionContext::Processor::Finish::ClosedOperationJob).to receive(:perform_at)
- expect(AuctionFunCore::Application[:event]).to have_received(:publish).once
+ operation
+
+ expect(AuctionFunCore::Workers::Operations::AuctionContext::Processor::Finish::ClosedOperationJob)
+ .to have_received(:perform_at)
+ .once
+ end
end
- context "when auction kind is penny" do
+ context "when auction kind is equal to 'penny'" do
let(:stopwatch) { 45 }
let(:old_finished_at) { auction.finished_at.strftime("%Y-%m-%d %H:%M:%S") }
let(:new_finished_at) { stopwatch.seconds.from_now.strftime("%Y-%m-%d %H:%M:%S") }
@@ -119,6 +130,16 @@
.from(old_finished_at)
.to(new_finished_at)
end
+
+ it "expect call the finish penny auction job" do
+ allow(AuctionFunCore::Workers::Operations::AuctionContext::Processor::Finish::PennyOperationJob)
+ .to receive(:perform_at)
+
+ operation
+
+ expect(AuctionFunCore::Workers::Operations::AuctionContext::Processor::Finish::PennyOperationJob)
+ .to have_received(:perform_at)
+ end
end
end
end
diff --git a/spec/auction_fun_core/operations/bid_context/create_bid_penny_operation_spec.rb b/spec/auction_fun_core/operations/bid_context/create_bid_penny_operation_spec.rb
index 58a67a3..fdb3209 100644
--- a/spec/auction_fun_core/operations/bid_context/create_bid_penny_operation_spec.rb
+++ b/spec/auction_fun_core/operations/bid_context/create_bid_penny_operation_spec.rb
@@ -3,6 +3,7 @@
require "spec_helper"
RSpec.describe AuctionFunCore::Operations::BidContext::CreateBidPennyOperation, type: :operation do
+ let(:auction_repository) { AuctionFunCore::Repos::AuctionContext::AuctionRepository.new }
let(:bid_repository) { AuctionFunCore::Repos::BidContext::BidRepository.new }
describe ".call(attributes, &block)" do
@@ -81,13 +82,52 @@
expect(operation.failure).to be_blank
end
- it "expect persist new bid on database and dispatch event" do
+ it "expect persist new bid on database" do
+ expect { operation }.to change(bid_repository, :count).from(0).to(1)
+ end
+
+ it "expect dispatch event" do
allow(AuctionFunCore::Application[:event]).to receive(:publish)
- expect { operation }.to change(bid_repository, :count).from(0).to(1)
+ operation
expect(AuctionFunCore::Application[:event]).to have_received(:publish).once
end
+
+ context "when an auction has not started" do
+ it "expects not to update the auction's 'finished_at' field" do
+ expect { operation }.not_to change { auction_repository.by_id(auction.id).finished_at }
+ end
+
+ it "expect not reschedule the end of the auction" do
+ allow(AuctionFunCore::Workers::Operations::AuctionContext::Processor::Finish::PennyOperationJob)
+ .to receive(:perform_at).with(Time, auction.id)
+
+ operation
+
+ expect(AuctionFunCore::Workers::Operations::AuctionContext::Processor::Finish::PennyOperationJob)
+ .not_to have_received(:perform_at).with(Time, auction.id)
+ end
+ end
+
+ context "when an auction was started" do
+ let(:auction) { Factory[:auction, :default_running_penny] }
+ let(:user) { Factory[:user, :with_balance] }
+
+ it "expects to update the auction's 'finished_at' field and reschedule the end of the auction" do
+ expect { operation }.to change { auction_repository.by_id(auction.id).finished_at }
+ end
+
+ it "expect reschedule the end of the auction" do
+ allow(AuctionFunCore::Workers::Operations::AuctionContext::Processor::Finish::PennyOperationJob)
+ .to receive(:perform_at).with(Time, auction.id)
+
+ operation
+
+ expect(AuctionFunCore::Workers::Operations::AuctionContext::Processor::Finish::PennyOperationJob)
+ .to have_received(:perform_at).with(Time, auction.id).once
+ end
+ end
end
end
end
diff --git a/spec/auction_fun_core/services/mail/auction_context/post_auction/participant_mailer_spec.rb b/spec/auction_fun_core/services/mail/auction_context/post_auction/participant_mailer_spec.rb
new file mode 100644
index 0000000..c3a1a1a
--- /dev/null
+++ b/spec/auction_fun_core/services/mail/auction_context/post_auction/participant_mailer_spec.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe AuctionFunCore::Services::Mail::AuctionContext::PostAuction::ParticipantMailer, type: :mailer do
+ let(:default_email_system) { AuctionFunCore::Application[:settings].default_email_system }
+
+ describe "#deliver" do
+ subject(:mailer) { described_class.new(auction, participant, statistics) }
+
+ context "when participant has invalid data" do
+ let(:participant) { Factory.structs[:user, id: 1, email: nil] }
+ let(:auction) { Factory.structs[:auction, id: 1] }
+ let(:statistics) { OpenStruct.new(auction_date: Date.current) }
+
+ it "expect raise error" do
+ expect { mailer.deliver }.to raise_error(
+ ArgumentError, "SMTP To address may not be blank: []"
+ )
+ end
+ end
+
+ context "when participant has valid data" do
+ let(:winner) { Factory[:user] }
+ let(:participant) { Factory[:user] }
+ let(:auction) { Factory[:auction, :default_finished_standard, winner_id: winner.id] }
+ let(:statistics) do
+ OpenStruct.new(
+ auction_total_bids: 2, winner_bid: (auction.minimal_bid_cents * 2), winner_total_bids: 1,
+ auction_date: Date.current
+ )
+ end
+
+ subject(:mailer) { described_class.new(auction, participant, statistics).deliver }
+
+ it "expect send email with correct data" do
+ expect(mailer).to be_a_instance_of(Mail::Message)
+ expect(mail_from(default_email_system)).to be_truthy
+ expect(
+ sent_mail_to?(
+ participant.email,
+ I18n.t("mail.auction_context.post_auction.participant_mailer.subject", title: auction.title)
+ )
+ ).to be_truthy
+ end
+ end
+ end
+end
diff --git a/spec/auction_fun_core/services/mail/auction_context/post_auction/winner_mailer_spec.rb b/spec/auction_fun_core/services/mail/auction_context/post_auction/winner_mailer_spec.rb
new file mode 100644
index 0000000..8ea7584
--- /dev/null
+++ b/spec/auction_fun_core/services/mail/auction_context/post_auction/winner_mailer_spec.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe AuctionFunCore::Services::Mail::AuctionContext::PostAuction::WinnerMailer, type: :mailer do
+ let(:default_email_system) { AuctionFunCore::Application[:settings].default_email_system }
+
+ describe "#deliver" do
+ subject(:mailer) { described_class.new(auction, winner, statistics) }
+
+ context "when winner has invalid data" do
+ let(:winner) { Factory.structs[:user, email: nil] }
+ let(:auction) { Factory.structs[:auction, id: 1] }
+ let(:statistics) { OpenStruct.new(auction_date: Date.current) }
+
+ it "expect raise error" do
+ expect { mailer.deliver }.to raise_error(
+ ArgumentError, "SMTP To address may not be blank: []"
+ )
+ end
+ end
+
+ context "when winner has valid data" do
+ let(:winner) { Factory[:user] }
+ let(:auction) { Factory[:auction, :default_finished_standard, winner_id: winner.id] }
+ let(:bid) { Factory[:bid, auction_id: auction.id, user_id: winner.id, value_cents: auction.minimal_bid_cents] }
+ let(:statistics) do
+ OpenStruct.new(
+ auction_total_bids: 1, winner_bid: auction.minimal_bid_cents, winner_total_bids: 1,
+ auction_date: Date.current
+ )
+ end
+
+ subject(:mailer) { described_class.new(auction, winner, statistics).deliver }
+
+ it "expect send email with correct data" do
+ expect(mailer).to be_a_instance_of(Mail::Message)
+ expect(mail_from(default_email_system)).to be_truthy
+ expect(
+ sent_mail_to?(
+ winner.email,
+ I18n.t("mail.auction_context.post_auction.winner_mailer.subject", title: auction.title)
+ )
+ ).to be_truthy
+ end
+ end
+ end
+end
diff --git a/spec/auction_fun_core/workers/operations/auction_context/post_auction/participation_operation_job_spec.rb b/spec/auction_fun_core/workers/operations/auction_context/post_auction/participation_operation_job_spec.rb
new file mode 100644
index 0000000..933e3ad
--- /dev/null
+++ b/spec/auction_fun_core/workers/operations/auction_context/post_auction/participation_operation_job_spec.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe AuctionFunCore::Workers::Operations::AuctionContext::PostAuction::ParticipantOperationJob, type: :worker do
+ let(:auction_repository) { AuctionFunCore::Repos::AuctionContext::AuctionRepository.new }
+ let(:participant) { Factory[:user] }
+ let(:auction) { Factory[:auction, :default_standard, :with_winner] }
+
+ describe "#perform" do
+ subject(:worker) { described_class.new }
+
+ context "when params are valid" do
+ before do
+ Factory[:bid, auction_id: auction.id, user_id: auction.winner_id]
+ Factory[:bid, auction_id: auction.id, user_id: participant.id]
+ end
+
+ it "expect execute participant mailer service" do
+ allow(AuctionFunCore::Workers::Services::Mail::AuctionContext::PostAuction::ParticipantMailerJob).to receive(:perform_async)
+
+ worker.perform(auction.id, participant.id)
+
+ expect(AuctionFunCore::Workers::Services::Mail::AuctionContext::PostAuction::ParticipantMailerJob).to have_received(:perform_async).once
+ end
+ end
+
+ context "when an exception occours but retry limit is not reached" do
+ before do
+ stub_const("::AuctionFunCore::Workers::ApplicationJob::MAX_RETRIES", 1)
+ allow(AuctionFunCore::Application[:logger]).to receive(:error)
+ end
+
+ it "expect rescue/capture exception and reschedule job" do
+ expect { worker.perform(nil, nil) }.to change(described_class.jobs, :size).from(0).to(1)
+
+ expect(AuctionFunCore::Application[:logger]).to have_received(:error).at_least(:once)
+ end
+ end
+
+ context "when the exception reaches the retry limit" do
+ before do
+ stub_const("::AuctionFunCore::Workers::ApplicationJob::MAX_RETRIES", 0)
+ allow(AuctionFunCore::Application[:logger]).to receive(:error)
+ end
+
+ it "expect raise exception and stop retry" do
+ expect { worker.perform(nil, nil) }.to raise_error(ROM::TupleCountMismatchError)
+
+ expect(AuctionFunCore::Application[:logger]).to have_received(:error).at_least(:once)
+ end
+ end
+ end
+end
diff --git a/spec/auction_fun_core/workers/operations/auction_context/post_auction/winner_operation_job_spec.rb b/spec/auction_fun_core/workers/operations/auction_context/post_auction/winner_operation_job_spec.rb
new file mode 100644
index 0000000..6f2f893
--- /dev/null
+++ b/spec/auction_fun_core/workers/operations/auction_context/post_auction/winner_operation_job_spec.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe AuctionFunCore::Workers::Operations::AuctionContext::PostAuction::WinnerOperationJob, type: :worker do
+ let(:auction_repository) { AuctionFunCore::Repos::AuctionContext::AuctionRepository.new }
+ let(:auction) { Factory[:auction, :default_standard, :with_winner] }
+
+ describe "#perform" do
+ subject(:worker) { described_class.new }
+
+ context "when params are valid" do
+ it "expect execute winner mailer service" do
+ allow(AuctionFunCore::Workers::Services::Mail::AuctionContext::PostAuction::WinnerMailerJob).to receive(:perform_async)
+
+ worker.perform(auction.id, auction.winner_id)
+
+ expect(AuctionFunCore::Workers::Services::Mail::AuctionContext::PostAuction::WinnerMailerJob).to have_received(:perform_async).once
+ end
+ end
+
+ context "when an exception occours but retry limit is not reached" do
+ before do
+ stub_const("::AuctionFunCore::Workers::ApplicationJob::MAX_RETRIES", 1)
+ allow(AuctionFunCore::Application[:logger]).to receive(:error)
+ end
+
+ it "expect rescue/capture exception and reschedule job" do
+ expect { worker.perform(nil, nil) }.to change(described_class.jobs, :size).from(0).to(1)
+
+ expect(AuctionFunCore::Application[:logger]).to have_received(:error).at_least(:once)
+ end
+ end
+
+ context "when the exception reaches the retry limit" do
+ before do
+ stub_const("::AuctionFunCore::Workers::ApplicationJob::MAX_RETRIES", 0)
+ allow(AuctionFunCore::Application[:logger]).to receive(:error)
+ end
+
+ it "expect raise exception and stop retry" do
+ expect { worker.perform(nil, nil) }.to raise_error(ROM::TupleCountMismatchError)
+
+ expect(AuctionFunCore::Application[:logger]).to have_received(:error).at_least(:once)
+ end
+ end
+ end
+end
diff --git a/spec/auction_fun_core/workers/operations/auction_context/processor/finish/closed_operation_job_spec.rb b/spec/auction_fun_core/workers/operations/auction_context/processor/finish/closed_operation_job_spec.rb
new file mode 100644
index 0000000..fd53f0a
--- /dev/null
+++ b/spec/auction_fun_core/workers/operations/auction_context/processor/finish/closed_operation_job_spec.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe AuctionFunCore::Workers::Operations::AuctionContext::Processor::Finish::ClosedOperationJob, type: :worker do
+ let(:auction_repository) { AuctionFunCore::Repos::AuctionContext::AuctionRepository.new }
+ let(:auction) { Factory[:auction, :default_running_closed] }
+
+ describe "#perform" do
+ subject(:worker) { described_class.new }
+
+ context "when params are valid" do
+ it "expect execute auction start operation" do
+ expect { worker.perform(auction.id) }
+ .to change { auction_repository.by_id(auction.id).status }
+ .from("running")
+ .to("finished")
+ end
+ end
+
+ context "when an exception occours but retry limit is not reached" do
+ before do
+ stub_const("::AuctionFunCore::Workers::ApplicationJob::MAX_RETRIES", 1)
+ allow(AuctionFunCore::Application[:logger]).to receive(:error)
+ end
+
+ it "expect rescue/capture exception and reschedule job" do
+ expect { worker.perform(nil) }.to change(described_class.jobs, :size).from(0).to(1)
+
+ expect(AuctionFunCore::Application[:logger]).to have_received(:error).at_least(:once)
+ end
+ end
+
+ context "when the exception reaches the retry limit" do
+ before do
+ stub_const("::AuctionFunCore::Workers::ApplicationJob::MAX_RETRIES", 0)
+ allow(AuctionFunCore::Application[:logger]).to receive(:error)
+ end
+
+ it "expect raise exception and stop retry" do
+ expect { worker.perform(nil) }.to raise_error(ROM::TupleCountMismatchError)
+
+ expect(AuctionFunCore::Application[:logger]).to have_received(:error).at_least(:once)
+ end
+ end
+ end
+end
diff --git a/spec/auction_fun_core/workers/operations/auction_context/processor/finish/penny_operation_job_spec.rb b/spec/auction_fun_core/workers/operations/auction_context/processor/finish/penny_operation_job_spec.rb
new file mode 100644
index 0000000..aa6e2f6
--- /dev/null
+++ b/spec/auction_fun_core/workers/operations/auction_context/processor/finish/penny_operation_job_spec.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe AuctionFunCore::Workers::Operations::AuctionContext::Processor::Finish::PennyOperationJob, type: :worker do
+ let(:auction_repository) { AuctionFunCore::Repos::AuctionContext::AuctionRepository.new }
+ let(:auction) { Factory[:auction, :default_running_penny] }
+
+ describe "#perform" do
+ subject(:worker) { described_class.new }
+
+ context "when params are valid" do
+ it "expect execute auction start operation" do
+ expect { worker.perform(auction.id) }
+ .to change { auction_repository.by_id(auction.id).status }
+ .from("running")
+ .to("finished")
+ end
+ end
+
+ context "when an exception occours but retry limit is not reached" do
+ before do
+ stub_const("::AuctionFunCore::Workers::ApplicationJob::MAX_RETRIES", 1)
+ allow(AuctionFunCore::Application[:logger]).to receive(:error)
+ end
+
+ it "expect rescue/capture exception and reschedule job" do
+ expect { worker.perform(nil) }.to change(described_class.jobs, :size).from(0).to(1)
+
+ expect(AuctionFunCore::Application[:logger]).to have_received(:error).at_least(:once)
+ end
+ end
+
+ context "when the exception reaches the retry limit" do
+ before do
+ stub_const("::AuctionFunCore::Workers::ApplicationJob::MAX_RETRIES", 0)
+ allow(AuctionFunCore::Application[:logger]).to receive(:error)
+ end
+
+ it "expect raise exception and stop retry" do
+ expect { worker.perform(nil) }.to raise_error(ROM::TupleCountMismatchError)
+
+ expect(AuctionFunCore::Application[:logger]).to have_received(:error).at_least(:once)
+ end
+ end
+ end
+end
diff --git a/spec/auction_fun_core/workers/operations/auction_context/processor/finish_operation_job_spec.rb b/spec/auction_fun_core/workers/operations/auction_context/processor/finish/standard_operation_job_spec.rb
similarity index 96%
rename from spec/auction_fun_core/workers/operations/auction_context/processor/finish_operation_job_spec.rb
rename to spec/auction_fun_core/workers/operations/auction_context/processor/finish/standard_operation_job_spec.rb
index 84ad179..9a4d5ff 100644
--- a/spec/auction_fun_core/workers/operations/auction_context/processor/finish_operation_job_spec.rb
+++ b/spec/auction_fun_core/workers/operations/auction_context/processor/finish/standard_operation_job_spec.rb
@@ -2,7 +2,7 @@
require "spec_helper"
-RSpec.describe AuctionFunCore::Workers::Operations::AuctionContext::Processor::FinishOperationJob, type: :worker do
+RSpec.describe AuctionFunCore::Workers::Operations::AuctionContext::Processor::Finish::StandardOperationJob, type: :worker do
let(:auction_repository) { AuctionFunCore::Repos::AuctionContext::AuctionRepository.new }
let(:auction) { Factory[:auction, :default_running_standard] }
diff --git a/spec/auction_fun_core/workers/services/mail/auction_context/post_auction/participant_mailer_job_spec.rb b/spec/auction_fun_core/workers/services/mail/auction_context/post_auction/participant_mailer_job_spec.rb
new file mode 100644
index 0000000..1e8937f
--- /dev/null
+++ b/spec/auction_fun_core/workers/services/mail/auction_context/post_auction/participant_mailer_job_spec.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe AuctionFunCore::Workers::Services::Mail::AuctionContext::PostAuction::ParticipantMailerJob, type: :worker do
+ let(:auction_repository) { AuctionFunCore::Repos::AuctionContext::AuctionRepository.new }
+ let(:user_repository) { AuctionFunCore::Repos::UserContext::UserRepository.new }
+ let(:relation) { AuctionFunCore::Application[:container].relations[:auctions] }
+ let(:participant) { Factory[:user] }
+ let(:auction) { Factory[:auction, :default_finished_standard, :with_winner] }
+ let(:statistics) { ROM::OpenStruct.new(id: auction.id, auction_total_bids: 0, winner_bid: nil, winner_total_bids: 0) }
+ let(:participant_mailer) { AuctionFunCore::Services::Mail::AuctionContext::PostAuction::ParticipantMailer }
+ let(:mailer) { participant_mailer.new(auction, participant, statistics) }
+
+ describe "#perform" do
+ subject(:worker) { described_class.new }
+
+ context "when attributes are valid" do
+ before do
+ allow(AuctionFunCore::Repos::AuctionContext::AuctionRepository).to receive(:new).and_return(auction_repository)
+ allow(AuctionFunCore::Repos::UserContext::UserRepository).to receive(:new).and_return(user_repository)
+ allow(auction_repository).to receive(:by_id!).with(auction.id).and_return(auction)
+ allow(user_repository).to receive(:by_id!).with(participant.id).and_return(participant)
+ allow(relation).to receive_message_chain("load_participant_statistics.call.first").and_return(statistics)
+ allow(participant_mailer).to receive(:new).with(auction, participant, statistics).and_return(mailer)
+ allow(mailer).to receive(:deliver).and_return(true)
+ end
+
+ it "expect trigger registration mailer service" do
+ worker.perform(auction.id, participant.id)
+
+ expect(mailer).to have_received(:deliver).once
+ end
+ end
+
+ context "when an exception occours but retry limit is not reached" do
+ before do
+ stub_const("::AuctionFunCore::Workers::ApplicationJob::MAX_RETRIES", 1)
+ allow(AuctionFunCore::Application[:logger]).to receive(:error)
+ end
+
+ it "expect rescue/capture exception and reschedule job" do
+ expect { worker.perform(nil, nil) }.to change(described_class.jobs, :size).from(0).to(1)
+
+ expect(AuctionFunCore::Application[:logger]).to have_received(:error).at_least(:once)
+ end
+ end
+
+ context "when the exception reaches the retry limit" do
+ before do
+ stub_const("::AuctionFunCore::Workers::ApplicationJob::MAX_RETRIES", 0)
+ allow(AuctionFunCore::Application[:logger]).to receive(:error)
+ end
+
+ it "expect raise exception and stop retry" do
+ expect { worker.perform(nil, nil) }.to raise_error(ROM::TupleCountMismatchError)
+
+ expect(AuctionFunCore::Application[:logger]).to have_received(:error).at_least(:once)
+ end
+ end
+ end
+end
diff --git a/spec/auction_fun_core/workers/services/mail/auction_context/post_auction/winner_mailer_job_spec.rb b/spec/auction_fun_core/workers/services/mail/auction_context/post_auction/winner_mailer_job_spec.rb
new file mode 100644
index 0000000..eb8e73a
--- /dev/null
+++ b/spec/auction_fun_core/workers/services/mail/auction_context/post_auction/winner_mailer_job_spec.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe AuctionFunCore::Workers::Services::Mail::AuctionContext::PostAuction::WinnerMailerJob, type: :worker do
+ let(:auction_repository) { AuctionFunCore::Repos::AuctionContext::AuctionRepository.new }
+ let(:user_repository) { AuctionFunCore::Repos::UserContext::UserRepository.new }
+ let(:relation) { AuctionFunCore::Application[:container].relations[:auctions] }
+ let(:winner) { Factory[:user] }
+ let(:auction) { Factory[:auction, :default_finished_standard, :with_winner] }
+ let(:statistics) { ROM::OpenStruct.new(id: auction.id, auction_total_bids: 1, winner_bid: auction.minimal_bid_cents, winner_total_bids: 1) }
+ let(:winner_mailer) { AuctionFunCore::Services::Mail::AuctionContext::PostAuction::WinnerMailer }
+ let(:mailer) { winner_mailer.new(auction, winner, statistics) }
+
+ describe "#perform" do
+ subject(:worker) { described_class.new }
+
+ context "when attributes are valid" do
+ before do
+ allow(AuctionFunCore::Repos::AuctionContext::AuctionRepository).to receive(:new).and_return(auction_repository)
+ allow(AuctionFunCore::Repos::UserContext::UserRepository).to receive(:new).and_return(user_repository)
+ allow(auction_repository).to receive(:by_id!).with(auction.id).and_return(auction)
+ allow(user_repository).to receive(:by_id!).with(winner.id).and_return(winner)
+ allow(relation).to receive_message_chain("load_winner_statistics.call.first").and_return(statistics)
+ allow(winner_mailer).to receive(:new).with(auction, winner, statistics).and_return(mailer)
+ allow(mailer).to receive(:deliver).and_return(true)
+ end
+
+ it "expect trigger registration mailer service" do
+ worker.perform(auction.id, winner.id)
+
+ expect(mailer).to have_received(:deliver).once
+ end
+ end
+
+ context "when an exception occours but retry limit is not reached" do
+ before do
+ stub_const("::AuctionFunCore::Workers::ApplicationJob::MAX_RETRIES", 1)
+ allow(AuctionFunCore::Application[:logger]).to receive(:error)
+ end
+
+ it "expect rescue/capture exception and reschedule job" do
+ expect { worker.perform(nil, nil) }.to change(described_class.jobs, :size).from(0).to(1)
+
+ expect(AuctionFunCore::Application[:logger]).to have_received(:error).at_least(:once)
+ end
+ end
+
+ context "when the exception reaches the retry limit" do
+ before do
+ stub_const("::AuctionFunCore::Workers::ApplicationJob::MAX_RETRIES", 0)
+ allow(AuctionFunCore::Application[:logger]).to receive(:error)
+ end
+
+ it "expect raise exception and stop retry" do
+ expect { worker.perform(nil, nil) }.to raise_error(ROM::TupleCountMismatchError)
+
+ expect(AuctionFunCore::Application[:logger]).to have_received(:error).at_least(:once)
+ end
+ end
+ end
+end
diff --git a/spec/auction_fun_core_spec.rb b/spec/auction_fun_core_spec.rb
index 7213020..db22df5 100644
--- a/spec/auction_fun_core_spec.rb
+++ b/spec/auction_fun_core_spec.rb
@@ -2,6 +2,6 @@
RSpec.describe AuctionFunCore do
it "has a version number" do
- expect(AuctionFunCore::VERSION).to eq("0.8.5")
+ expect(AuctionFunCore::VERSION).to eq("0.8.6")
end
end
diff --git a/spec/support/factories/auctions.rb b/spec/support/factories/auctions.rb
index a109494..e136d13 100644
--- a/spec/support/factories/auctions.rb
+++ b/spec/support/factories/auctions.rb
@@ -12,6 +12,14 @@
f.trait :with_minimal_bid do |t|
end
+ f.trait :with_winner do |t|
+ f.association(:winner)
+ end
+
+ f.trait :with_participants do |t|
+ f.association(:winner)
+ end
+
f.trait :with_kind_standard do |t|
t.kind { "standard" }
end
@@ -101,6 +109,7 @@
t.stopwatch { AuctionFunCore::Business::Configuration::AUCTION_STOPWATCH_MIN_VALUE }
t.started_at { 1.hour.from_now }
+ t.finished_at { 1.hour.from_now + AuctionFunCore::Business::Configuration::AUCTION_STOPWATCH_MIN_VALUE }
end
f.trait :default_running_penny do |t|
diff --git a/system/providers/background_job.rb b/system/providers/background_job.rb
index 229839e..be4b104 100644
--- a/system/providers/background_job.rb
+++ b/system/providers/background_job.rb
@@ -3,13 +3,32 @@
AuctionFunCore::Application.register_provider(:background_job) do
prepare do
require "sidekiq"
+ require "sidekiq-unique-jobs"
end
start do
Sidekiq.configure_server do |config|
+ config.redis = {url: target[:settings].redis_url}
config.logger = target[:settings].logger
config.average_scheduled_poll_interval = 3
+
+ config.client_middleware do |chain|
+ chain.add SidekiqUniqueJobs::Middleware::Client
+ end
+
+ config.server_middleware do |chain|
+ chain.add SidekiqUniqueJobs::Middleware::Server
+ end
+
+ SidekiqUniqueJobs::Server.configure(config)
+ end
+
+ Sidekiq.configure_client do |config|
config.redis = {url: target[:settings].redis_url}
+
+ config.client_middleware do |chain|
+ chain.add SidekiqUniqueJobs::Middleware::Client
+ end
end
end
end