diff --git a/CHANGELOG.md b/CHANGELOG.md index f528b80..3f83d73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ ## [Unreleased] +## [0.8.8] - 2024-04-25 + +### Added + +- Auction start reminder sent to participants; +- I18n time format; + +### Fixed: + +- I18n messages for winner and auction participant emails; + ## [0.8.7] - 2024-04-23 ### Added diff --git a/Gemfile.lock b/Gemfile.lock index 7669caf..50860b9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - auction_fun_core (0.8.7) + auction_fun_core (0.8.8) activesupport (= 7.1.3.2) bcrypt (= 3.1.20) dotenv (= 3.1.0) diff --git a/i18n/en-US/contracts/contracts.en-US.yml b/i18n/en-US/contracts/contracts.en-US.yml index f6c615e..14732ac 100644 --- a/i18n/en-US/contracts/contracts.en-US.yml +++ b/i18n/en-US/contracts/contracts.en-US.yml @@ -70,6 +70,9 @@ en-US: auction_context: create: finished_at: "must be after started time" + pre_auction: + auction_start_reminder: + auction_already_started: "auction already started" post_auction: participant: none: "there was no participation from this user in the auction reported" diff --git a/i18n/en-US/mail/application.en-US.yml b/i18n/en-US/mail/application.en-US.yml index ccd6a90..4c0cd62 100644 --- a/i18n/en-US/mail/application.en-US.yml +++ b/i18n/en-US/mail/application.en-US.yml @@ -52,6 +52,13 @@ en-US: - :year - :month - :day + time: + am: am + formats: + default: "%a, %d %b %Y %H:%M:%S %z" + long: "%B %d, %Y %H:%M" + short: "%d %b %H:%M" + pm: pm application: general: hello: "Hi %{name}" diff --git a/i18n/en-US/mail/auction_context/pre_auction/auction_start_reminder.en-US.yml b/i18n/en-US/mail/auction_context/pre_auction/auction_start_reminder.en-US.yml new file mode 100644 index 0000000..d10d00b --- /dev/null +++ b/i18n/en-US/mail/auction_context/pre_auction/auction_start_reminder.en-US.yml @@ -0,0 +1,11 @@ +pt-BR: + mail: + auction_context: + pre_auction: + auction_start_reminder_mailer: + subject: "The %{title} auction will start soon!" + body: + description: "We are excited to inform you that the auction you are participating in will begin soon!" + auction_date: "Start Time" + link_to_auction: "Click the link below to view auction details" + thanks: "Thank you for your participation and we wish you good luck!" diff --git a/i18n/pt-BR/contracts/contracts.pt-BR.yml b/i18n/pt-BR/contracts/contracts.pt-BR.yml index 278ef2b..f34e8df 100644 --- a/i18n/pt-BR/contracts/contracts.pt-BR.yml +++ b/i18n/pt-BR/contracts/contracts.pt-BR.yml @@ -70,6 +70,9 @@ pt-BR: auction_context: create: finished_at: "deve ser depois da hora de início" + pre_auction: + auction_start_reminder: + auction_already_started: "leilão já foi iniciado" post_auction: participant: none: "não houve nenhuma participação deste usuário no leilão informado" diff --git a/i18n/pt-BR/mail/application.pt-BR.yml b/i18n/pt-BR/mail/application.pt-BR.yml index bb3993d..75be72e 100644 --- a/i18n/pt-BR/mail/application.pt-BR.yml +++ b/i18n/pt-BR/mail/application.pt-BR.yml @@ -52,6 +52,13 @@ pt-BR: - :day - :month - :year + time: + am: am + formats: + default: "%a, %d de %B de %Y, %H:%M:%S %z" + long: "%d de %B de %Y, %H:%M" + short: "%d de %B, %H:%M" + pm: pm application: general: hello: "Olá %{name}" diff --git a/i18n/pt-BR/mail/auction_context/pre_auction/auction_start_reminder.pt-BR.yml b/i18n/pt-BR/mail/auction_context/pre_auction/auction_start_reminder.pt-BR.yml new file mode 100644 index 0000000..605f0ab --- /dev/null +++ b/i18n/pt-BR/mail/auction_context/pre_auction/auction_start_reminder.pt-BR.yml @@ -0,0 +1,11 @@ +pt-BR: + mail: + auction_context: + pre_auction: + auction_start_reminder_mailer: + subject: "O leilão %{title} começará em breve!" + body: + description: "Estamos entusiasmados em informar que o leilão em que você está participando começará em breve!" + auction_date: "Hora de Início" + link_to_auction: "Clique no link abaixo para ver os detalhes do leilão" + thanks: "Agradecemos pela sua participação e desejamos boa sorte!" diff --git a/lib/auction_fun_core/contracts/auction_context/pre_auction/auction_start_reminder_contract.rb b/lib/auction_fun_core/contracts/auction_context/pre_auction/auction_start_reminder_contract.rb new file mode 100644 index 0000000..d7a4e04 --- /dev/null +++ b/lib/auction_fun_core/contracts/auction_context/pre_auction/auction_start_reminder_contract.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module AuctionFunCore + module Contracts + module AuctionContext + module PreAuction + ## + # Contract class for validate schedule reminder notification. + # + class AuctionStartReminderContract < Contracts::ApplicationContract + I18N_SCOPE = "contracts.errors.custom.auction_context.pre_auction.auction_start_reminder" + + option :auction_repository, default: proc { Repos::AuctionContext::AuctionRepository.new } + + params do + required(:auction_id).filled(:integer) + end + + rule(:auction_id) do |context:| + context[:auction] ||= auction_repository.by_id(value) + key.failure(I18n.t("contracts.errors.custom.not_found")) unless context[:auction] + end + + # Validation to start. + # Checks whether the auction has started or not. + # + rule do |context:| + next if context[:auction].present? && context[:auction].not_started? + + key(:base).failure(I18n.t("auction_already_started", scope: I18N_SCOPE)) + end + end + end + end + end +end diff --git a/lib/auction_fun_core/entities/auction.rb b/lib/auction_fun_core/entities/auction.rb index df4ed71..1278ba5 100644 --- a/lib/auction_fun_core/entities/auction.rb +++ b/lib/auction_fun_core/entities/auction.rb @@ -7,17 +7,62 @@ module Entities class Auction < ROM::Struct INQUIRER_ATTRIBUTES = Relations::Auctions::STATUSES.values.freeze + # Retrieves the initial bid amount for an auction as a Money object. + # + # This method creates and returns a new Money object that represents the initial bid + # amount required to start bidding in the auction. It utilizes `initial_bid_cents` and + # `initial_bid_currency` attributes to construct the Money object, ensuring that the amount + # is correctly represented in the specified currency. + # + # @return [Money] Returns a Money object representing the initial bid amount with the specified currency. def initial_bid Money.new(initial_bid_cents, initial_bid_currency) end + # Retrieves the minimal bid amount for an auction as a Money object. + # + # This method creates and returns a new Money object that represents the minimal bid + # amount required to participate in the auction. It uses `minimal_bid_cents` and + # `minimal_bid_currency` attributes to construct the Money object. + # + # @return [Money] Returns a Money object representing the minimal bid amount with the appropriate currency. def minimal_bid Money.new(minimal_bid_cents, minimal_bid_currency) end + # Checks if an auction has a winner. + # + # This method determines if an auction has a winner based on the presence of a `winner_id`. + # It returns true if the `winner_id` is present, indicating that the auction has concluded + # with a winning bidder. + # + # @return [Boolean] Returns `true` if there is a winner for the auction, otherwise returns `false`. def winner? winner_id.present? end + + # Checks if an auction has already started. + # + # This method determines if an auction has begun based on its status and by comparing + # the auction's start time (`started_at`) with the current time. An auction is considered + # started if it is no longer scheduled (i.e., its status is not "scheduled") and + # the start time (`started_at`) is equal to or before the current time. + # @return [Boolean] Returns `true` if the auction has already started, otherwise returns `false`. + def started? + status != "scheduled" && Time.current > started_at + end + + # Checks if an auction has not started yet. + # + # This method verifies if an auction is still in the "scheduled" status and whether + # its start time (`started_at`) is still in the future compared to the current time. + # The auction is considered not started if it is scheduled and the start time + # has not yet been reached. + # + # @return [Boolean] Returns `true` if the auction has not started yet, otherwise returns `false`. + def not_started? + status == "scheduled" && Time.current <= started_at + end end end end diff --git a/lib/auction_fun_core/operations/auction_context/create_operation.rb b/lib/auction_fun_core/operations/auction_context/create_operation.rb index 8b7f4f8..ccdd768 100644 --- a/lib/auction_fun_core/operations/auction_context/create_operation.rb +++ b/lib/auction_fun_core/operations/auction_context/create_operation.rb @@ -10,6 +10,7 @@ class CreateOperation < AuctionFunCore::Operations::Base include Import["repos.auction_context.auction_repository"] include Import["contracts.auction_context.create_contract"] include Import["workers.operations.auction_context.processor.start_operation_job"] + include Import["workers.operations.auction_context.pre_auction.auction_start_reminder_operation_job"] # @todo Add custom doc def self.call(attributes, &block) @@ -27,6 +28,7 @@ def call(attributes) auction_repository.transaction do |_t| @auction = yield persist(values) yield scheduled_start_auction(@auction) + yield schedule_auction_notification(@auction) yield publish_auctions_created(@auction) end @@ -63,15 +65,6 @@ def persist(result) Success(auction_repository.create(result)) end - # Triggers the publication of event *auctions.created*. - # @param auction [Hash] Auction persisted attributes - # @return [Dry::Monads::Result::Success] - def publish_auctions_created(auction) - Application[:event].publish("auctions.created", auction.to_h) - - Success() - end - # Calls the background job class that will schedule the start of the auction. # Added a small delay to perform operations (such as sending broadcasts and/or other operations). # @param auction [ROM::Struct::Auction] @@ -81,6 +74,29 @@ def scheduled_start_auction(auction) Success(start_operation_job.class.perform_at(perform_at, auction.id)) end + + # Schedules a notification to be sent to users one hour before the auction starts. + # The scheduling is only done if the start of the auction is more than one hour ahead of the current time, + # ensuring that there is sufficient time for the notification to be sent. + # + # @param auction [ROM::Struct::Auction] + # @return [String] sidekiq jid + def schedule_auction_notification(auction) + perform_time = auction.started_at - 1.hour + + return Success() if perform_time <= Time.current + + Success(auction_start_reminder_operation_job.class.perform_at(perform_time, auction.id)) + end + + # Triggers the publication of event *auctions.created*. + # @param auction [ROM::Struct::Auction] + # @return [Dry::Monads::Result::Success] + def publish_auctions_created(auction) + Application[:event].publish("auctions.created", auction.to_h) + + Success() + end end end end diff --git a/lib/auction_fun_core/operations/auction_context/pre_auction/auction_start_reminder_operation.rb b/lib/auction_fun_core/operations/auction_context/pre_auction/auction_start_reminder_operation.rb new file mode 100644 index 0000000..007f646 --- /dev/null +++ b/lib/auction_fun_core/operations/auction_context/pre_auction/auction_start_reminder_operation.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +module AuctionFunCore + module Operations + module AuctionContext + module PreAuction + ## + # Operation class for send a reminder email to a participant about the start of an auction. + # + class AuctionStartReminderOperation < AuctionFunCore::Operations::Base + include Import["repos.bid_context.bid_repository"] + include Import["contracts.auction_context.pre_auction.auction_start_reminder_contract"] + include Import["workers.services.mail.auction_context.pre_auction.auction_start_reminder_mailer_job"] + + # @todo Add custom doc + def self.call(attributes, &block) + operation = new.call(attributes) + + return operation unless block + + Dry::Matcher::ResultMatcher.call(operation, &block) + end + + def call(attributes) + auction = yield validate_contract(attributes) + participant_ids = yield collect_current_auction_participants(auction.id) + + bid_repository.transaction do |_t| + participant_ids.each do |participant_id| + yield send_auction_start_reminder_mailer_job(auction.id, participant_id) + end + end + + Success([auction, participant_ids]) + end + + private + + def validate_contract(attributes) + contract = auction_start_reminder_contract.call(attributes) + + return Failure(contract.errors.to_h) if contract.failure? + + Success(contract.context[:auction]) + end + + def collect_current_auction_participants(auction_id) + Success( + AuctionFunCore::Application[:container] + .relations[:bids] + .participants(auction_id) + .one + .participant_ids.to_a + ) + end + + def send_auction_start_reminder_mailer_job(auction_id, participant_id) + Success(auction_start_reminder_mailer_job.class.perform_async(auction_id, participant_id)) + end + end + end + end + end +end diff --git a/lib/auction_fun_core/relations/bids.rb b/lib/auction_fun_core/relations/bids.rb index 2049308..eb11c0a 100644 --- a/lib/auction_fun_core/relations/bids.rb +++ b/lib/auction_fun_core/relations/bids.rb @@ -23,6 +23,24 @@ class Bids < ROM::Relation[:sql] struct_namespace Entities auto_struct(true) + + # Retrieves a list of unique user IDs who have placed bids in a specified auction. + # A participant in an auction is defined as a user who has placed one or more bids. + # + # @param auction_id [Integer] the ID of the auction. + # @return [Array] Returns an array of unique user IDs who have participated in the auction. + # @raise [RuntimeError] Raises an error if the auction_id is not an integer. + def participants(auction_id) + raise "Invalid argument" unless auction_id.is_a?(Integer) + + sql = <<-SQL + SELECT COALESCE(ARRAY_AGG(DISTINCT user_id), ARRAY[]::INT[]) AS participant_ids + FROM bids + WHERE auction_id = #{auction_id} + SQL + + read(sql) + end end end end diff --git a/lib/auction_fun_core/services/mail/auction_context/pre_auction/auction_start_reminder_mailer.rb b/lib/auction_fun_core/services/mail/auction_context/pre_auction/auction_start_reminder_mailer.rb new file mode 100644 index 0000000..a111082 --- /dev/null +++ b/lib/auction_fun_core/services/mail/auction_context/pre_auction/auction_start_reminder_mailer.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module AuctionFunCore + module Services + module Mail + module AuctionContext + module PreAuction + class AuctionStartReminderMailer + include IdleMailer::Mailer + include IdleMailer::TemplateManager + + # @param auction [ROM::Struct::Auction] The auction object + # @param participant [ROM::Struct::User] The participant object + def initialize(auction, participant) + @auction = auction + @participant = participant + mail.to = participant.email + mail.subject = I18n.t("mail.auction_context.pre_auction.auction_start_reminder_mailer.subject", title: @auction.title) + end + + def self.template_name + IdleMailer.config.templates.join("auction_context/pre_auction/auction_start_reminder") + 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 index 6a2eee3..327ff66 100644 --- 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 @@ -29,7 +29,7 @@

- <%= I18n.t("mail.general.hello", name: @participant.name) %>, + <%= I18n.t("application.general.hello", name: @participant.name) %>,

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 index 1226fc7..8ebac7e 100644 --- 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 @@ -29,7 +29,7 @@

- <%= I18n.t("mail.general.hello", name: @winner.name) %>, + <%= I18n.t("application.general.hello", name: @winner.name) %>,

diff --git a/lib/auction_fun_core/services/mail/templates/auction_context/pre_auction/auction_start_reminder.html.erb b/lib/auction_fun_core/services/mail/templates/auction_context/pre_auction/auction_start_reminder.html.erb new file mode 100644 index 0000000..4375b68 --- /dev/null +++ b/lib/auction_fun_core/services/mail/templates/auction_context/pre_auction/auction_start_reminder.html.erb @@ -0,0 +1,192 @@ + + + + +
+
+ + + + + + +
+ + + + +
+
+ + + + + + + + + +
+ + + + + + +
+ image description +
+
+
+

+ <%= I18n.t("application.general.hello", name: @participant.name) %>, +

+
+
+
+
+
+
+
+ + + + +
+
+ + + + + + +
+ + + + +
+
+ + + + + + + + + + + + + + + + + + + + + +
+
+ <%= raw I18n.t("mail.auction_context.pre_auction.auction_start_reminder_mailer.body.description") %> +
+
+
+
    +
  • <%= I18n.t("mail.auction_context.pre_auction.auction_start_reminder_mailer.body.auction_date") %>: <%= I18n.l(@auction.started_at, format: :long) %>
  • +
+
+ + + + + + +
+ <%= I18n.t("mail.auction_context.pre_auction.auction_start_reminder_mailer.body.link_to_auction") %> +
+
+
+

<%= I18n.t("mail.auction_context.pre_auction.auction_start_reminder_mailer.body.thanks") %>

+
+
+
+ <%= I18n.t("application.general.team") %> +
+
+ + + + + + +
+ + + + + + +
+ + + + + + +
+ + twitter-logo + +
+
+
+ + + + + + +
+ + + + + + +
+ + facebook-logo + +
+
+
+ + + + + + +
+ + + + + + +
+ + instagram-logo + +
+
+
+
+
+
+
+
+
diff --git a/lib/auction_fun_core/version.rb b/lib/auction_fun_core/version.rb index 9d83e3d..98a073b 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.7" + VERSION = "0.8.8" # Required class module is a gem dependency class Version; end diff --git a/lib/auction_fun_core/workers/operations/auction_context/pre_auction/auction_start_reminder_operation_job.rb b/lib/auction_fun_core/workers/operations/auction_context/pre_auction/auction_start_reminder_operation_job.rb new file mode 100644 index 0000000..e1c2b4b --- /dev/null +++ b/lib/auction_fun_core/workers/operations/auction_context/pre_auction/auction_start_reminder_operation_job.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +module AuctionFunCore + module Workers + module Operations + module AuctionContext + module PreAuction + ## + # BackgroundJob class for call auction start reminder operation. + class AuctionStartReminderOperationJob < Workers::ApplicationJob + include Import["repos.user_context.user_repository"] + include Import["repos.auction_context.auction_repository"] + + # @todo Add detailed documentation + def perform(auction_id, retry_count = 0) + auction = auction_repository.by_id!(auction_id) + + auction_start_reminder_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 + + 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 auction_start_reminder_operation + AuctionFunCore::Operations::AuctionContext::PreAuction::AuctionStartReminderOperation + end + end + end + end + end + end +end diff --git a/lib/auction_fun_core/workers/services/mail/auction_context/pre_auction/auction_start_reminder_mailer_job.rb b/lib/auction_fun_core/workers/services/mail/auction_context/pre_auction/auction_start_reminder_mailer_job.rb new file mode 100644 index 0000000..0d349ac --- /dev/null +++ b/lib/auction_fun_core/workers/services/mail/auction_context/pre_auction/auction_start_reminder_mailer_job.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module AuctionFunCore + module Workers + module Services + module Mail + module AuctionContext + module PreAuction + ## + # Background job class responsible for adding emails to the queue. + # + class AuctionStartReminderMailerJob < 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) + + auction_start_reminder_mailer.new(auction, participant).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 auction_start_reminder_mailer + AuctionFunCore::Services::Mail::AuctionContext::PreAuction::AuctionStartReminderMailer + 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 index 1c06497..f173f91 100644 --- 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 @@ -67,7 +67,7 @@ Factory[:bid, auction: auction, user_id: participant.id, value_cents: auction.minimal_bid_cents] end - it "expect return sucess" do + it "expect return success" 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) diff --git a/spec/auction_fun_core/contracts/auction_context/pre_auction/auction_start_reminder_contract_spec.rb b/spec/auction_fun_core/contracts/auction_context/pre_auction/auction_start_reminder_contract_spec.rb new file mode 100644 index 0000000..38822e5 --- /dev/null +++ b/spec/auction_fun_core/contracts/auction_context/pre_auction/auction_start_reminder_contract_spec.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +RSpec.describe AuctionFunCore::Contracts::AuctionContext::PreAuction::AuctionStartReminderContract, type: :contract do + describe "#call" do + subject(:contract) { described_class.new.call(attributes) } + + 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?")) + end + end + + context "when auction is not found on database" do + let(:attributes) { {auction_id: 2_234_231} } + + 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 the auction has already started" do + let(:auction) { Factory[:auction, :default_finished_standard, started_at: 3.hours.ago] } + let(:attributes) { {auction_id: auction.id} } + + it "expect failure with error messages" do + expect(contract.errors[:base]).to include( + I18n.t("contracts.errors.custom.auction_context.pre_auction.auction_start_reminder.auction_already_started") + ) + end + end + + context "when the auction has not started" do + let(:auction) { Factory[:auction, :default_scheduled_standard, started_at: 3.hours.from_now] } + 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 +end diff --git a/spec/auction_fun_core/entities/auction_spec.rb b/spec/auction_fun_core/entities/auction_spec.rb index b8f9c0b..171bd07 100644 --- a/spec/auction_fun_core/entities/auction_spec.rb +++ b/spec/auction_fun_core/entities/auction_spec.rb @@ -36,4 +36,40 @@ end end end + + describe "#started?" do + context "when an auction has not started" do + subject(:auction) { Factory.structs[:auction, :default_scheduled_standard, started_at: 3.hours.from_now] } + + it "expect return false" do + expect(auction.started?).to be_falsey + end + end + + context "when an auction was started" do + subject(:auction) { Factory.structs[:auction, :default_running_standard, started_at: 1.minute.ago] } + + it "expect return true" do + expect(auction.started?).to be_truthy + end + end + end + + describe "#not_started?" do + context "when an auction was started" do + subject(:auction) { Factory.structs[:auction, :default_running_standard, started_at: 1.minute.ago] } + + it "expect return false" do + expect(auction.not_started?).to be_falsey + end + end + + context "when an auction has not started" do + subject(:auction) { Factory.structs[:auction, :default_scheduled_standard, started_at: 3.hours.from_now] } + + it "expect return true" do + expect(auction.not_started?).to be_truthy + 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 684bebf..aa769b2 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 @@ -90,6 +90,37 @@ .to have_received(:perform_at) .once end + + context "when the start of the auction is more than 1h from the current time" do + before do + attributes[:started_at] = 2.hours.from_now + allow(AuctionFunCore::Workers::Operations::AuctionContext::PreAuction::AuctionStartReminderOperationJob) + .to receive(:perform_at) + end + + it "expect schedule reminder notification" do + operation + + expect(AuctionFunCore::Workers::Operations::AuctionContext::PreAuction::AuctionStartReminderOperationJob) + .to have_received(:perform_at) + .once + end + end + + context "When the start of the auction is less than 1h from the current time" do + before do + attributes[:started_at] = 30.minutes.from_now + allow(AuctionFunCore::Workers::Operations::AuctionContext::PreAuction::AuctionStartReminderOperationJob) + .to receive(:perform_at) + end + + it "expect not schedule reminder notification" do + operation + + expect(AuctionFunCore::Workers::Operations::AuctionContext::PreAuction::AuctionStartReminderOperationJob) + .not_to have_received(:perform_at) + end + end end end end diff --git a/spec/auction_fun_core/operations/auction_context/pre_auction/auction_start_reminder_operation_spec.rb b/spec/auction_fun_core/operations/auction_context/pre_auction/auction_start_reminder_operation_spec.rb new file mode 100644 index 0000000..581ba08 --- /dev/null +++ b/spec/auction_fun_core/operations/auction_context/pre_auction/auction_start_reminder_operation_spec.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe AuctionFunCore::Operations::AuctionContext::PreAuction::AuctionStartReminderOperation, type: :operation do + describe ".call(attributes, &block)" do + let(:operation) { described_class } + + context "when block is given" do + context "when operation happens with success" do + let(:auction) { Factory[:auction, :default_scheduled_standard, started_at: 3.hours.from_now] } + let(:participant) { Factory[:user] } + let(:attributes) { {auction_id: auction.id} } + + before do + 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.id]) + 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(attributes)" 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?")) + end + end + + context "when contract is valid" do + let(:auction) { Factory[:auction, :default_scheduled_standard, started_at: 3.hours.from_now] } + let(:attributes) { {auction_id: auction.id} } + + context "when the auction has no participants" do + it "expect not to schedule a reminder email to the participant" do + allow(AuctionFunCore::Workers::Services::Mail::AuctionContext::PreAuction::AuctionStartReminderMailerJob) + .to receive(:perform_async) + + operation + + expect(AuctionFunCore::Workers::Services::Mail::AuctionContext::PreAuction::AuctionStartReminderMailerJob) + .not_to have_received(:perform_async) + end + end + + context "when the auction has participants" do + let(:participant) { Factory[:user] } + + before do + Factory[:bid, user_id: participant.id, auction_id: auction.id, value_cents: auction.minimal_bid_cents] + end + + it "expects to schedule a reminder email to the participant" do + allow(AuctionFunCore::Workers::Services::Mail::AuctionContext::PreAuction::AuctionStartReminderMailerJob) + .to receive(:perform_async).with(auction.id, participant.id) + + operation + + expect(AuctionFunCore::Workers::Services::Mail::AuctionContext::PreAuction::AuctionStartReminderMailerJob) + .to have_received(:perform_async).with(auction.id, participant.id).once + end + end + end + end +end diff --git a/spec/auction_fun_core/services/mail/auction_context/pre_auction/auction_start_reminder_mailer_spec.rb b/spec/auction_fun_core/services/mail/auction_context/pre_auction/auction_start_reminder_mailer_spec.rb new file mode 100644 index 0000000..397d64d --- /dev/null +++ b/spec/auction_fun_core/services/mail/auction_context/pre_auction/auction_start_reminder_mailer_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe AuctionFunCore::Services::Mail::AuctionContext::PreAuction::AuctionStartReminderMailer, type: :mailer do + let(:default_email_system) { AuctionFunCore::Application[:settings].default_email_system } + + describe "#deliver" do + subject(:mailer) { described_class.new(auction, participant) } + + let(:auction) { Factory.structs[:auction, :default_scheduled_standard, id: 1, started_at: 3.hours.from_now] } + + context "when participant has invalid data" do + let(:participant) { Factory.structs[:user, id: 1, email: nil] } + + 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(:participant) { Factory[:user] } + + subject(:mailer) { described_class.new(auction, participant).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.pre_auction.auction_start_reminder_mailer.subject", title: auction.title) + ) + ).to be_truthy + end + end + end +end diff --git a/spec/auction_fun_core/workers/operations/auction_context/pre_auction/auction_start_reminder_operation_job_spec.rb b/spec/auction_fun_core/workers/operations/auction_context/pre_auction/auction_start_reminder_operation_job_spec.rb new file mode 100644 index 0000000..548a2a8 --- /dev/null +++ b/spec/auction_fun_core/workers/operations/auction_context/pre_auction/auction_start_reminder_operation_job_spec.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe AuctionFunCore::Workers::Operations::AuctionContext::PreAuction::AuctionStartReminderOperationJob, type: :worker do + let(:auction) { Factory[:auction, :default_scheduled_standard, started_at: 3.hours.from_now] } + let(:operation_class) { AuctionFunCore::Operations::AuctionContext::PreAuction::AuctionStartReminderOperation } + + describe "#perform" do + subject(:worker) { described_class.new } + + context "when params are valid" do + it "expect execute auction start reminder operation" do + allow(AuctionFunCore::Operations::AuctionContext::PreAuction::AuctionStartReminderOperation) + .to receive(:call) + .with(auction_id: auction.id) + + worker.perform(auction.id) + + expect(AuctionFunCore::Operations::AuctionContext::PreAuction::AuctionStartReminderOperation) + .to have_received(:call) + .with(auction_id: auction.id) + .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) }.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/services/mail/auction_context/post_auction/pre_auction/auction_start_reminder_mailer_job_spec.rb b/spec/auction_fun_core/workers/services/mail/auction_context/post_auction/pre_auction/auction_start_reminder_mailer_job_spec.rb new file mode 100644 index 0000000..c7dc2d8 --- /dev/null +++ b/spec/auction_fun_core/workers/services/mail/auction_context/post_auction/pre_auction/auction_start_reminder_mailer_job_spec.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe AuctionFunCore::Workers::Services::Mail::AuctionContext::PreAuction::AuctionStartReminderMailerJob, type: :worker do + let(:auction_repository) { AuctionFunCore::Repos::AuctionContext::AuctionRepository.new } + let(:user_repository) { AuctionFunCore::Repos::UserContext::UserRepository.new } + let(:participant) { Factory[:user] } + let(:auction) { Factory[:auction, :default_finished_standard, :with_winner] } + let(:auction_start_reminder_mailer) { AuctionFunCore::Services::Mail::AuctionContext::PreAuction::AuctionStartReminderMailer } + let(:mailer) { auction_start_reminder_mailer.new(auction, participant) } + + 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(auction_start_reminder_mailer).to receive(:new).with(auction, participant).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_spec.rb b/spec/auction_fun_core_spec.rb index 68fdabc..d66ab81 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.7") + expect(AuctionFunCore::VERSION).to eq("0.8.8") end end