diff --git a/app/services/users_credits/manager.rb b/app/services/users_credits/manager.rb new file mode 100644 index 000000000..451b825ab --- /dev/null +++ b/app/services/users_credits/manager.rb @@ -0,0 +1,136 @@ +require 'forwardable' + +module UsersCredits + class AlreadyUpdatedError < StandardError;end; + + class Manager + extend Forwardable + attr_reader :manager + + def initialize(reservation: nil, user: nil) + if user + @manager = Managers::User.new(user) + elsif reservation + if reservation.reservable_type == "Training" + @manager = Managers::Training.new(reservation) + elsif reservation.reservable_type == "Machine" + @manager = Managers::Machine.new(reservation) + end + end + end + + def_delegators :@manager, :will_use_credits?, :free_hours_count, :update_credits, :reset_credits + end + + module Managers + class User + attr_reader :user + + def initialize(user) + @user = user + end + + def reset_credits + user.users_credits.destroy_all + end + end + + class Reservation + attr_reader :reservation + + def initialize(reservation) + @reservation = reservation + @already_updated = false + end + + def plan + user.subscribed_plan + end + + def user + reservation.user + end + + def update_credits + if @already_updated + raise AlreadyUpdatedError, "update credit is not idempotent ! you can't invoke update_credits method twice." + else + @already_updated = true + end + end + end + private_constant :Reservation + + + class Machine < Reservation + def will_use_credits? + _will_use_credits?[0] + end + + def free_hours_count + _will_use_credits?[1] + end + + def update_credits + super + + will_use_credits, free_hours_count, machine_credit = _will_use_credits? + if will_use_credits + users_credit = user.users_credits.find_or_initialize_by(credit_id: machine_credit.id) + + if users_credit.new_record? + users_credit.hours_used = free_hours_count + else + users_credit.hours_used += free_hours_count + end + users_credit.save! + end + end + + private + def _will_use_credits? + if machine_credit = plan.machine_credits.find_by(creditable_id: reservation.reservable_id) + users_credit = user.users_credits.find_by(credit_id: machine_credit.id) + already_used_hours = users_credit ? users_credit.hours_used : 0 + + remaining_hours = machine_credit.hours - already_used_hours + + free_hours_count = [remaining_hours, reservation.slots.size].min + + if free_hours_count > 0 + return true, free_hours_count, machine_credit + else + return false, free_hours_count, machine_credit + end + end + return false, 0 + end + end + + class Training < Reservation + def will_use_credits? + _will_use_credits?[0] + end + + def update_credits + super + will_use_credits, training_credit = _will_use_credits? + if will_use_credits + user.credits << training_credit # we create a new UsersCredit object + end + end + + private + def _will_use_credits? + # if there is a training_credit defined for this plan and this training + if training_credit = plan.training_credits.find_by(creditable_id: reservation.reservable_id) + # if user has not used all the plan credits + if user.training_credits.count < plan.training_credit_nb + return true, training_credit + end + end + return false, nil + end + end + end +end diff --git a/app/services/users_credits_manager.rb b/app/services/users_credits_manager.rb deleted file mode 100644 index 76f228417..000000000 --- a/app/services/users_credits_manager.rb +++ /dev/null @@ -1,82 +0,0 @@ -class UsersCreditsManager - attr_reader :manager - - def initialize(reservation) - if reservation.reservable_type == "Training" - @manager = Training.new(reservation) - elsif reservation.reservable_type == "Machine" - @manager = Machine.new(reservation) - end - end - - class Machine < Manager - def will_use_credits? - end - - def credited_hours_number - end - - def update - super - end - - private - def _will_use_credits? - end - end - - class Training < Manager - def will_use_credits? - _will_use_credits?[0] - end - - def update - super - will_use_credits, training_credit = _will_use_credits? - if will_use_credits - user.credits << training_credit # we create a new UsersCredit object - end - end - - private - def _will_use_credits? - # if there is a training_credit defined for this plan and this training - if training_credit = plan.training_credits.find_by(creditable_id: reservation.reservable_id) - # if user has not used all the plan credits - if user.training_credits.count < plan.training_credit_nb - return true, training_credit - end - end - return false, nil - end - end - - private - class Manager - attr_reader :reservation - - def initialize(reservation) - @reservation = reservation - @already_updated = false - end - - def plan - user.subscribed_plan - end - - def user - reservation.user - end - - def update - if @already_updated - raise AlreadyUpdated, "update credit is not idempotent ! do not try to update twice." - else - @already_updated = true - end - end - end - - class AlreadyUpdated < StandardError - end -end diff --git a/test/services/users_credits_manager_test.rb b/test/services/users_credits_manager_test.rb new file mode 100644 index 000000000..aa69eab83 --- /dev/null +++ b/test/services/users_credits_manager_test.rb @@ -0,0 +1,162 @@ +require 'test_helper' + +class UsersCreditsManagerTest < ActiveSupport::TestCase + setup do + @machine = Machine.find(6) + @training = Training.find(2) + @plan = Plan.find(3) + @user = User.joins(:subscriptions).find_by(subscriptions: { plan: @plan }) + @availability = @machine.availabilities.first + @reservation_machine = Reservation.new(user: @user, reservable: @machine) + @reservation_training = Reservation.new(user: @user, reservable: @training) + end + + ## context machine reservation + + test "machine reservation without credit associated" do + Credit.where(creditable: @machine).destroy_all + + @reservation_machine.assign_attributes(slots_attributes: [{ + start_at: @availability.start_at, end_at: @availability.start_at + 1.hour, availability_id: @availability.id + }]) + manager = UsersCredits::Manager.new(reservation: @reservation_machine) + + assert_equal false, manager.will_use_credits? + assert_equal 0, manager.free_hours_count + + assert_no_difference 'UsersCredit.count' do + manager.update_credits + end + + assert_raise UsersCredits::AlreadyUpdatedError do + manager.update_credits + end + end + + test "machine reservation with credit associated and user never used his credit" do + credit = Credit.find_by!(creditable: @machine, plan: @plan) + credit.update!(hours: 2) + @user.users_credits.destroy_all + + @reservation_machine.assign_attributes(slots_attributes: [{ + start_at: @availability.start_at, end_at: @availability.start_at + 1.hour, availability_id: @availability.id + }]) + manager = UsersCredits::Manager.new(reservation: @reservation_machine) + + assert_equal true, manager.will_use_credits? + assert_equal 1, manager.free_hours_count + + assert_difference 'UsersCredit.count' do + manager.update_credits + end + + assert_raise UsersCredits::AlreadyUpdatedError do + manager.update_credits + end + end + + test "machine reservation with credit associated and user already used partially his credit" do + credit = Credit.find_by!(creditable: @machine, plan: @plan) + credit.update!(hours: 2) + users_credit = @user.users_credits.create!(credit: credit, hours_used: 1) + + @reservation_machine.assign_attributes(slots_attributes: [ + { start_at: @availability.start_at, end_at: @availability.start_at + 1.hour, availability_id: @availability.id }, + { start_at: @availability.start_at + 1.hour, end_at: @availability.start_at + 2.hour, availability_id: @availability.id } + ]) + + manager = UsersCredits::Manager.new(reservation: @reservation_machine) + + assert_equal true, manager.will_use_credits? + assert_equal 1, manager.free_hours_count + + assert_no_difference 'UsersCredit.count' do + manager.update_credits + end + + users_credit.reload + assert_equal 2, users_credit.hours_used + end + + test "machine reservation with credit associated and user already used all credit" do + credit = Credit.find_by!(creditable: @machine, plan: @plan) + users_credit = @user.users_credits.create!(credit: credit, hours_used: 1) + + @reservation_machine.assign_attributes(slots_attributes: [ + { start_at: @availability.start_at, end_at: @availability.start_at + 1.hour, availability_id: @availability.id }, + { start_at: @availability.start_at + 1.hour, end_at: @availability.start_at + 2.hour, availability_id: @availability.id } + ]) + manager = UsersCredits::Manager.new(reservation: @reservation_machine) + + assert_equal false, manager.will_use_credits? + assert_equal 0, manager.free_hours_count + + assert_no_difference 'UsersCredit.count' do + manager.update_credits + end + + users_credit.reload + assert_equal 1, users_credit.hours_used + end + + # context training reservation + + test "training reservation without credit associated" do + Credit.where(creditable: @training).destroy_all + + manager = UsersCredits::Manager.new(reservation: @reservation_training) + + assert_equal false, manager.will_use_credits? + + assert_no_difference 'UsersCredit.count' do + manager.update_credits + end + + assert_raise UsersCredits::AlreadyUpdatedError do + manager.update_credits + end + end + + test "training reservation with credit associated and user didnt use his credit yet" do + credit = Credit.find_or_create_by!(creditable: @training, plan: @plan) + @user.users_credits.destroy_all + + manager = UsersCredits::Manager.new(reservation: @reservation_training) + + assert_equal true, manager.will_use_credits? + + assert_difference 'UsersCredit.count' do + manager.update_credits + end + end + + test "training reservation with credit associated but user already used all his credits" do + @user.users_credits.destroy_all + another_training = Training.where.not(id: @training.id).first + credit = Credit.find_or_create_by!(creditable: another_training, plan: @plan) + @user.users_credits.find_or_create_by!(credit: credit) + @plan.update(training_credit_nb: 1) + + manager = UsersCredits::Manager.new(reservation: @reservation_training) + + assert_equal false, manager.will_use_credits? + + assert_no_difference 'UsersCredit.count' do + manager.update_credits + end + end + + # context reset user credits + + test "use UsersCredit::Manager to reset users_credits" do + credit = Credit.find_by!(creditable: @machine, plan: @plan) + users_credit = @user.users_credits.create!(credit: credit, hours_used: 1) + + assert_not_empty @user.users_credits + + manager = UsersCredits::Manager.new(user: @user) + manager.reset_credits + + assert_empty @user.users_credits.reload + end +end