1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-02-20 14:54:15 +01:00

(quality) isolate auto cancel features in separate service

This commit is contained in:
Sylvain 2023-01-26 14:11:14 +01:00 committed by Sylvain
parent 7208cd80b0
commit 78cb8b7854
6 changed files with 228 additions and 127 deletions

View File

@ -105,7 +105,7 @@ class SettingService
deadline = settings.find { |s| s.name == 'trainings_auto_cancel_deadline' }
Training.find_each do |t|
TrainingService.update_auto_cancel(t, tac, threshold, deadline)
Trainings::AutoCancelService.update_auto_cancel(t, tac, threshold, deadline)
end
end
end

View File

@ -18,58 +18,6 @@ class TrainingService
trainings
end
# @param training [Training]
def auto_cancel_reservation(training)
return unless training.auto_cancel
training.availabilities
.includes(slots: :slots_reservations)
.where('availabilities.start_at >= ?', DateTime.current - training.auto_cancel_deadline.hours)
.find_each do |availability|
next if availability.reservations.count >= training.auto_cancel_threshold
auto_refund = Setting.get('wallet_module')
NotificationCenter.call type: 'notify_admin_training_auto_cancelled',
receiver: User.admins_and_managers,
attached_object: availability,
meta_data: { auto_refund: auto_refund }
availability.slots_reservations.find_each do |sr|
NotificationCenter.call type: 'notify_member_training_auto_cancelled',
receiver: sr.reservation.user,
attached_object: sr,
meta_data: { auto_refund: auto_refund }
sr.update(canceled_at: DateTime.current)
refund_after_cancel(sr.reservation) if auto_refund
end
end
end
# update the given training, depending on the provided settings
# @param training [Training]
# @param auto_cancel [Setting,NilClass]
# @param threshold [Setting,NilClass]
# @param deadline [Setting,NilClass]
def update_auto_cancel(training, auto_cancel, threshold, deadline)
previous_auto_cancel = auto_cancel.nil? ? Setting.find_by(name: 'trainings_auto_cancel').value : auto_cancel.previous_value
previous_threshold = threshold.nil? ? Setting.find_by(name: 'trainings_auto_cancel_threshold').value : threshold.previous_value
previous_deadline = deadline.nil? ? Setting.find_by(name: 'trainings_auto_cancel_deadline').value : deadline.previous_value
is_default = training.auto_cancel.to_s == previous_auto_cancel &&
[nil, previous_threshold].include?(training.auto_cancel_threshold.to_s) &&
[nil, previous_deadline].include?(training.auto_cancel_deadline.to_s)
return unless is_default
# update parameters if the given training is default
params = {}
params[:auto_cancel] = auto_cancel.value unless auto_cancel.nil?
params[:auto_cancel_threshold] = threshold.value unless threshold.nil?
params[:auto_cancel_deadline] = deadline.value unless deadline.nil?
training.update(params)
end
private
# @param trainings [ActiveRecord::Relation<Training>]
@ -89,13 +37,5 @@ class TrainingService
state = filters[:public_page] == 'false' ? [nil, false] : true
trainings.where(public_page: state)
end
# @param reservation [Reservation]
def refund_after_cancel(reservation)
invoice_item = reservation.invoice_items.joins(:invoice).where(invoices: { type: nil }).first
service = WalletService.new(user: reservation.user, wallet: reservation.user.wallet)
transaction = service.credit(invoice_item.amount_after_coupon / 100.00)
service.create_avoir(transaction, DateTime.current, I18n.t('trainings.refund_for_auto_cancel')) if transaction
end
end
end

View File

@ -0,0 +1,71 @@
# frozen_string_literal: true
# Business logic around trainings
module Trainings; end
# Automatically cancel trainings without enough reservation
class Trainings::AutoCancelService
class << self
# @param training [Training]
def auto_cancel_reservations(training)
return unless training.auto_cancel
training.availabilities
.includes(slots: :slots_reservations)
.where('availabilities.start_at >= ?', DateTime.current - training.auto_cancel_deadline.hours)
.find_each do |availability|
next if availability.reservations.count >= training.auto_cancel_threshold
auto_refund = Setting.get('wallet_module')
NotificationCenter.call type: 'notify_admin_training_auto_cancelled',
receiver: User.admins_and_managers,
attached_object: availability,
meta_data: { auto_refund: auto_refund }
availability.slots_reservations.find_each do |sr|
NotificationCenter.call type: 'notify_member_training_auto_cancelled',
receiver: sr.reservation.user,
attached_object: sr,
meta_data: { auto_refund: auto_refund }
sr.update(canceled_at: DateTime.current)
refund_after_cancel(sr.reservation) if auto_refund
end
end
end
# update the given training, depending on the provided settings
# @param training [Training]
# @param auto_cancel [Setting,NilClass]
# @param threshold [Setting,NilClass]
# @param deadline [Setting,NilClass]
def update_auto_cancel(training, auto_cancel, threshold, deadline)
previous_auto_cancel = auto_cancel.nil? ? Setting.find_by(name: 'trainings_auto_cancel').value : auto_cancel.previous_value
previous_threshold = threshold.nil? ? Setting.find_by(name: 'trainings_auto_cancel_threshold').value : threshold.previous_value
previous_deadline = deadline.nil? ? Setting.find_by(name: 'trainings_auto_cancel_deadline').value : deadline.previous_value
is_default = training.auto_cancel.to_s == previous_auto_cancel &&
[nil, previous_threshold].include?(training.auto_cancel_threshold.to_s) &&
[nil, previous_deadline].include?(training.auto_cancel_deadline.to_s)
return unless is_default
# update parameters if the given training is default
params = {}
params[:auto_cancel] = auto_cancel.value unless auto_cancel.nil?
params[:auto_cancel_threshold] = threshold.value unless threshold.nil?
params[:auto_cancel_deadline] = deadline.value unless deadline.nil?
training.update(params)
end
private
# @param reservation [Reservation]
def refund_after_cancel(reservation)
invoice_item = reservation.invoice_items.joins(:invoice).where(invoices: { type: nil }).first
service = WalletService.new(user: reservation.user, wallet: reservation.user.wallet)
transaction = service.credit(invoice_item.amount_after_coupon / 100.00)
service.create_avoir(transaction, DateTime.current, I18n.t('trainings.refund_for_auto_cancel')) if transaction
end
end
end

View File

@ -6,7 +6,7 @@ class TrainingAutoCancelWorker
def perform
Training.find_each do |t|
TrainingService.auto_cancel_reservation(t)
Trainings::AutoCancelService.auto_cancel_reservations(t)
end
end
end

View File

@ -1,65 +0,0 @@
# frozen_string_literal: true
require 'test_helper'
class TrainingServiceTest < ActiveSupport::TestCase
setup do
@training = Training.find(4)
@availability = Availability.find(22)
end
test 'auto cancel reservation with less reservations than the deadline' do
@training.update(auto_cancel: true, auto_cancel_threshold: 3, auto_cancel_deadline: 24)
customer = User.find(3)
slot = @availability.slots.first
r = Reservation.create!(
reservable_id: @training.id,
reservable_type: Training.name,
slots_reservations_attributes: [{ slot_id: slot.id }],
statistic_profile_id: StatisticProfile.find_by(user: customer).id
)
TrainingService.auto_cancel_reservation(@training)
r.reload
assert_not_nil r.slots_reservations.first&.canceled_at
end
test 'do not auto cancel reservation with more reservations than the deadline' do
@training.update(auto_cancel: true, auto_cancel_threshold: 3, auto_cancel_deadline: 24)
slot = @availability.slots.first
# first reservation
c1 = User.find(2)
r1 = Reservation.create!(
reservable_id: @training.id,
reservable_type: Training.name,
slots_reservations_attributes: [{ slot_id: slot.id }],
statistic_profile_id: StatisticProfile.find_by(user: c1).id
)
# second reservation
c2 = User.find(3)
r2 = Reservation.create!(
reservable_id: @training.id,
reservable_type: Training.name,
slots_reservations_attributes: [{ slot_id: slot.id }],
statistic_profile_id: StatisticProfile.find_by(user: c2).id
)
# third reservation
c3 = User.find(3)
r3 = Reservation.create!(
reservable_id: @training.id,
reservable_type: Training.name,
slots_reservations_attributes: [{ slot_id: slot.id }],
statistic_profile_id: StatisticProfile.find_by(user: c3).id
)
TrainingService.auto_cancel_reservation(@training)
r1.reload
assert_nil r1.slots_reservations.first&.canceled_at
r2.reload
assert_nil r2.slots_reservations.first&.canceled_at
r3.reload
assert_nil r3.slots_reservations.first&.canceled_at
end
end

View File

@ -0,0 +1,155 @@
# frozen_string_literal: true
require 'test_helper'
class Trainings::AutoCancelServiceTest < ActiveSupport::TestCase
setup do
@training = Training.find(4)
@availability = Availability.find(22)
end
test 'auto cancel reservation with less reservations than the threshold' do
Setting.set('wallet_module', false)
@training.update(auto_cancel: true, auto_cancel_threshold: 3, auto_cancel_deadline: 24)
customer = User.find(3)
slot = @availability.slots.first
r = Reservation.create!(
reservable_id: @training.id,
reservable_type: Training.name,
slots_reservations_attributes: [{ slot_id: slot.id }],
statistic_profile_id: StatisticProfile.find_by(user: customer).id
)
Trainings::AutoCancelService.auto_cancel_reservations(@training)
# Check reservation was cancelled
r.reload
assert_not_nil r.slots_reservations.first&.canceled_at
# Check notification was sent to the user
notification = Notification.find_by(
notification_type_id: NotificationType.find_by_name('notify_member_training_auto_cancelled'), # rubocop:disable Rails/DynamicFindBy
attached_object_type: 'SlotsReservation',
attached_object_id: r.slots_reservations.first&.id
)
assert_not_nil notification, 'user notification was not created'
assert_not notification.get_meta_data(:auto_refund)
# Check notification was sent to the admin
notification = Notification.find_by(
notification_type_id: NotificationType.find_by_name('notify_admin_training_auto_cancelled'), # rubocop:disable Rails/DynamicFindBy
attached_object_type: 'Availability',
attached_object_id: @availability.id
)
assert_not_nil notification, 'admin notification was not created'
assert_not notification.get_meta_data(:auto_refund)
end
test 'do not auto cancel reservation with more reservations than the threshold' do
@training.update(auto_cancel: true, auto_cancel_threshold: 3, auto_cancel_deadline: 24)
slot = @availability.slots.first
# first reservation
c1 = User.find(2)
r1 = Reservation.create!(
reservable_id: @training.id,
reservable_type: Training.name,
slots_reservations_attributes: [{ slot_id: slot.id }],
statistic_profile_id: StatisticProfile.find_by(user: c1).id
)
# second reservation
c2 = User.find(3)
r2 = Reservation.create!(
reservable_id: @training.id,
reservable_type: Training.name,
slots_reservations_attributes: [{ slot_id: slot.id }],
statistic_profile_id: StatisticProfile.find_by(user: c2).id
)
# third reservation
c3 = User.find(3)
r3 = Reservation.create!(
reservable_id: @training.id,
reservable_type: Training.name,
slots_reservations_attributes: [{ slot_id: slot.id }],
statistic_profile_id: StatisticProfile.find_by(user: c3).id
)
Trainings::AutoCancelService.auto_cancel_reservations(@training)
# Check nothing was cancelled
r1.reload
assert_nil r1.slots_reservations.first&.canceled_at
r2.reload
assert_nil r2.slots_reservations.first&.canceled_at
r3.reload
assert_nil r3.slots_reservations.first&.canceled_at
# Check no notifications were sent
assert_empty Notification.where(
notification_type_id: NotificationType.find_by_name('notify_member_training_auto_cancelled'), # rubocop:disable Rails/DynamicFindBy
attached_object_type: 'SlotsReservation',
attached_object_id: [r1.slots_reservations.first&.id, r2.slots_reservations.first&.id, r3.slots_reservations.first&.id]
)
assert_nil Notification.find_by(
notification_type_id: NotificationType.find_by_name('notify_admin_training_auto_cancelled'), # rubocop:disable Rails/DynamicFindBy
attached_object_type: 'Availability',
attached_object_id: @availability.id
)
end
test 'auto cancel reservation and generate refunds' do
Setting.set('wallet_module', true)
wallet_transactions = WalletTransaction.count
@training.update(auto_cancel: true, auto_cancel_threshold: 3, auto_cancel_deadline: 24)
customer = User.find(3)
slot = @availability.slots.first
# Reserve through the cart service to get an invoice associated with the reservation
cs = CartService.new(User.admins.first)
cs.from_hash(ActionController::Parameters.new({
customer_id: customer.id,
items: [
reservation: {
reservable_id: @training.id,
reservable_type: @training.class.name,
slots_reservations_attributes: [{ slot_id: slot.id }]
}
]
})).build_and_save(nil, nil)
# Go with cancelling
Trainings::AutoCancelService.auto_cancel_reservations(@training)
# Check reservation was cancelled
r = Reservation.last
assert_not_nil r.slots_reservations.first&.canceled_at
# Check notification was sent to the user
notification = Notification.find_by(
notification_type_id: NotificationType.find_by_name('notify_member_training_auto_cancelled'), # rubocop:disable Rails/DynamicFindBy
attached_object_type: 'SlotsReservation',
attached_object_id: r.slots_reservations.first&.id
)
assert_not_nil notification, 'user notification was not created'
assert notification.get_meta_data(:auto_refund)
# Check notification was sent to the admin
notification = Notification.find_by(
notification_type_id: NotificationType.find_by_name('notify_admin_training_auto_cancelled'), # rubocop:disable Rails/DynamicFindBy
attached_object_type: 'Availability',
attached_object_id: @availability.id
)
assert_not_nil notification, 'admin notification was not created'
assert notification.get_meta_data(:auto_refund)
# Check customer was refunded on his wallet
assert_equal wallet_transactions + 1, WalletTransaction.count
transaction = WalletTransaction.last
assert_equal transaction.wallet.user.id, customer.id
assert_equal transaction.transaction_type, 'credit'
assert_equal transaction.amount, r.invoice_items.first.amount
end
end