Skip to content

Commit

Permalink
Pre-Auction Notification for Participants (#42)
Browse files Browse the repository at this point in the history
  • Loading branch information
ricardopacheco authored Apr 25, 2024
1 parent 448b604 commit 6fdf60c
Show file tree
Hide file tree
Showing 29 changed files with 918 additions and 15 deletions.
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
## [Unreleased]

## [0.8.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
Expand Down
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
auction_fun_core (0.8.7)
auction_fun_core (0.8.8)
activesupport (= 7.1.3.2)
bcrypt (= 3.1.20)
dotenv (= 3.1.0)
Expand Down
3 changes: 3 additions & 0 deletions i18n/en-US/contracts/contracts.en-US.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
7 changes: 7 additions & 0 deletions i18n/en-US/mail/application.en-US.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Expand Down
Original file line number Diff line number Diff line change
@@ -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!"
3 changes: 3 additions & 0 deletions i18n/pt-BR/contracts/contracts.pt-BR.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
7 changes: 7 additions & 0 deletions i18n/pt-BR/mail/application.pt-BR.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Expand Down
Original file line number Diff line number Diff line change
@@ -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!"
Original file line number Diff line number Diff line change
@@ -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
45 changes: 45 additions & 0 deletions lib/auction_fun_core/entities/auction.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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

Expand Down Expand Up @@ -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]
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
18 changes: 18 additions & 0 deletions lib/auction_fun_core/relations/bids.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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<Integer>] 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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Helvetica, Arial, sans-serif;font-size:18px;font-weight:400;line-height:24px;text-align:left;color:#434245;">
<h1 style="margin: 0; font-size: 24px; line-height: normal; font-weight: bold;">
<%= I18n.t("mail.general.hello", name: @participant.name) %>,
<%= I18n.t("application.general.hello", name: @participant.name) %>,
</h1>
</div>
</td>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Helvetica, Arial, sans-serif;font-size:18px;font-weight:400;line-height:24px;text-align:left;color:#434245;">
<h1 style="margin: 0; font-size: 24px; line-height: normal; font-weight: bold;">
<%= I18n.t("mail.general.hello", name: @winner.name) %>,
<%= I18n.t("application.general.hello", name: @winner.name) %>,
</h1>
</div>
</td>
Expand Down
Loading

0 comments on commit 6fdf60c

Please sign in to comment.