Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[AF-39] Pre-Auction Notification for Participants #42

Merged
merged 1 commit into from
Apr 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
## [Unreleased]

## [0.8.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
Loading