1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-01-30 19:52:20 +01:00

(feat) training auto cancel authorization and invalidation rule

This commit is contained in:
Sylvain 2023-01-27 17:31:16 +01:00 committed by Sylvain
parent 78cb8b7854
commit ddc2dd4c6e
32 changed files with 407 additions and 101 deletions

View File

@ -29,7 +29,7 @@ class API::SettingsController < API::ApiController
updated_settings = []
may_transaction params[:transactional] do
params[:settings].each do |setting|
next if !setting[:name] || !setting[:value]
next if !setting[:name] || !setting[:value] || setting[:value].blank?
db_setting = Setting.find_or_initialize_by(name: setting[:name])
if !SettingService.update_allowed?(db_setting)

View File

@ -77,7 +77,8 @@ class API::TrainingsController < API::ApiController
def training_params
params.require(:training)
.permit(:id, :name, :description, :machine_ids, :plan_ids, :nb_total_places, :public_page, :disabled,
:auto_cancel, :auto_cancel_threshold, :auto_cancel_deadline,
:auto_cancel, :auto_cancel_threshold, :auto_cancel_deadline, :authorization, :authorization_period,
:invalidation, :invalidation_period,
training_image_attributes: %i[id attachment], machine_ids: [], plan_ids: [],
advanced_accounting_attributes: %i[code analytical_section])
end

View File

@ -39,12 +39,11 @@ export const TrainingForm: React.FC<TrainingFormProps> = ({ action, training, on
const [machineModule, setMachineModule] = useState<Setting>(null);
const [isActiveAccounting, setIsActiveAccounting] = useState<boolean>(false);
const [isActiveAuthorizationValidity, setIsActiveAuthorizationValidity] = useState<boolean>(false);
const [isActiveValidationRule, setIsActiveValidationRule] = useState<boolean>(false);
const { handleSubmit, register, control, setValue, formState } = useForm<Training>({ defaultValues: { ...training } });
const output = useWatch<Training>({ control });
const isActiveCancellation = useWatch({ control, name: 'auto_cancel' }) as boolean;
const isActiveAuthorizationValidity = useWatch({ control, name: 'authorization' }) as boolean;
const isActiveValidationRule = useWatch({ control, name: 'invalidation' });
useEffect(() => {
SettingAPI.get('machines_module').then(setMachineModule).catch(onError);
@ -79,20 +78,6 @@ export const TrainingForm: React.FC<TrainingFormProps> = ({ action, training, on
}).catch(error => onError(error));
};
/**
* Callback triggered when the authorisation validity switch has changed.
*/
const toggleAuthorizationValidity = (value: boolean) => {
setIsActiveAuthorizationValidity(value);
};
/**
* Callback triggered when the authorisation validity switch has changed.
*/
const toggleValidationRule = (value: boolean) => {
setIsActiveValidationRule(value);
};
return (
<div className="training-form">
<header>
@ -203,12 +188,12 @@ export const TrainingForm: React.FC<TrainingFormProps> = ({ action, training, on
<p className="description">{t('app.admin.training_form.authorization_validity_info')}</p>
</header>
<div className="content">
<FormSwitch id="authorization_validity" control={control}
onChange={toggleAuthorizationValidity} formState={formState}
<FormSwitch id="authorization" control={control}
formState={formState}
defaultValue={isActiveAuthorizationValidity}
label={t('app.admin.training_form.authorization_validity_switch')} />
{isActiveAuthorizationValidity && <>
<FormInput id="authorization_validity_duration"
<FormInput id="authorization_period"
type="number"
register={register}
rules={{ required: isActiveAuthorizationValidity, min: 1 }}
@ -225,12 +210,12 @@ export const TrainingForm: React.FC<TrainingFormProps> = ({ action, training, on
<p className="description">{t('app.admin.training_form.validation_rule_info')}</p>
</header>
<div className="content">
<FormSwitch id="validation_rule" control={control}
onChange={toggleValidationRule} formState={formState}
<FormSwitch id="invalidation" control={control}
formState={formState}
defaultValue={isActiveValidationRule}
label={t('app.admin.training_form.validation_rule_switch')} />
{isActiveValidationRule && <>
<FormInput id="validation_rule_period"
<FormInput id="invalidation_period"
type="number"
register={register}
rules={{ required: isActiveValidationRule, min: 1 }}

View File

@ -1,5 +1,5 @@
import * as React from 'react';
import { useEffect, useState } from 'react';
import { useEffect } from 'react';
import { IApplication } from '../../models/application';
import { Loader } from '../base/loader';
import { react2angular } from 'react2angular';
@ -29,8 +29,8 @@ export const TrainingsSettings: React.FC<TrainingsSettingsProps> = ({ onError, o
const { register, control, formState, handleSubmit, reset } = useForm<Record<SettingName, SettingValue>>();
const isActiveAutoCancellation = useWatch({ control, name: 'trainings_auto_cancel' }) as boolean;
const [isActiveAuthorizationValidity, setIsActiveAuthorizationValidity] = useState<boolean>(false);
const [isActiveValidationRule, setIsActiveValidationRule] = useState<boolean>(false);
const isActiveAuthorizationValidity = useWatch({ control, name: 'trainings_authorization_validity' }) as boolean;
const isActiveInvalidationRule = useWatch({ control, name: 'trainings_invalidation_rule' }) as boolean;
useEffect(() => {
SettingAPI.query(trainingSettings)
@ -41,20 +41,6 @@ export const TrainingsSettings: React.FC<TrainingsSettingsProps> = ({ onError, o
.catch(onError);
}, []);
/**
* Callback triggered when the authorisation validity switch has changed.
*/
const toggleAuthorizationValidity = (value: boolean) => {
setIsActiveAuthorizationValidity(value);
};
/**
* Callback triggered when the authorisation validity switch has changed.
*/
const toggleValidationRule = (value: boolean) => {
setIsActiveValidationRule(value);
};
/**
* Callback triggered when the form is submitted: save the settings
*/
@ -119,12 +105,12 @@ export const TrainingsSettings: React.FC<TrainingsSettingsProps> = ({ onError, o
<p className="description">{t('app.admin.trainings_settings.authorization_validity_info')}</p>
</header>
<div className="content">
<FormSwitch id="authorization_validity" control={control}
onChange={toggleAuthorizationValidity} formState={formState}
<FormSwitch id="trainings_authorization_validity" control={control}
formState={formState}
defaultValue={isActiveAuthorizationValidity}
label={t('app.admin.trainings_settings.authorization_validity_switch')} />
{isActiveAuthorizationValidity && <>
<FormInput id="authorization_validity_duration"
<FormInput id="trainings_authorization_validity_duration"
type="number"
register={register}
rules={{ required: isActiveAuthorizationValidity, min: 1 }}
@ -141,15 +127,15 @@ export const TrainingsSettings: React.FC<TrainingsSettingsProps> = ({ onError, o
<p className="description">{t('app.admin.trainings_settings.validation_rule_info')}</p>
</header>
<div className="content">
<FormSwitch id="validation_rule" control={control}
onChange={toggleValidationRule} formState={formState}
defaultValue={isActiveValidationRule}
<FormSwitch id="trainings_invalidation_rule" control={control}
formState={formState}
defaultValue={isActiveInvalidationRule}
label={t('app.admin.trainings_settings.validation_rule_switch')} />
{isActiveValidationRule && <>
<FormInput id="validation_rule_period"
{isActiveInvalidationRule && <>
<FormInput id="trainings_invalidation_rule_period"
type="number"
register={register}
rules={{ required: isActiveValidationRule, min: 1 }}
rules={{ required: isActiveInvalidationRule, min: 1 }}
step={1}
formState={formState}
label={t('app.admin.trainings_settings.validation_rule_period')} />

View File

@ -159,16 +159,16 @@ export const Trainings: React.FC<TrainingsProps> = ({ onError, onSuccess }) => {
<div className='authorisation'>
<span>{t('app.admin.trainings.authorisation')}</span>
<p>
{(training.authorization && <p>
{t('app.admin.trainings.active_true')}
<span>|</span>{t('app.admin.trainings.period_MONTH', { MONTH: 48 })}
</p>
<span>|</span>{t('app.admin.trainings.period_MONTH', { MONTH: training.authorization_period })}
</p>) || <p>---</p>}
</div>
<div className='rule'>
<span>{t('app.admin.trainings.validation_rule')}</span>
<p>
{t('app.admin.trainings.active_false')}
{training.invalidation ? t('app.admin.trainings.active_true') : t('app.admin.trainings.active_false')}
</p>
</div>

View File

@ -236,7 +236,11 @@ export const storeSettings = [
export const trainingSettings = [
'trainings_auto_cancel',
'trainings_auto_cancel_threshold',
'trainings_auto_cancel_deadline'
'trainings_auto_cancel_deadline',
'trainings_authorization_validity',
'trainings_authorization_validity_duration',
'trainings_invalidation_rule',
'trainings_invalidation_rule_period'
] as const;
export const allSettings = [

View File

@ -17,6 +17,10 @@ export interface Training {
auto_cancel: boolean,
auto_cancel_threshold: number,
auto_cancel_deadline: number,
authorization: boolean,
authorization_period: number,
invalidation: boolean,
invalidation_period: number,
availabilities?: Array<{
id: number,
start_at: TDateISO,
@ -33,5 +37,5 @@ export interface Training {
export interface TrainingIndexFilter extends ApiFilter {
disabled?: boolean,
public_page?: boolean,
requested_attributes?: ['availabillities'],
requested_attributes?: ['availabilities'],
}

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
# Add resources-related functionalities to the user model (eg. Reservation, Subscrtion, Project, etc.)
# Add resources-related functionalities to the user model (eg. Reservation, Subscription, Project, etc.)
module UserRessourcesConcern
extend ActiveSupport::Concern

View File

@ -75,6 +75,8 @@ class NotificationType
notify_admin_low_stock_threshold
notify_admin_training_auto_cancelled
notify_member_training_auto_cancelled
notify_member_training_authorization_expired
notify_member_training_invalidated
]
# deprecated:
# - notify_member_subscribed_plan_is_changed

View File

@ -168,7 +168,11 @@ class Setting < ApplicationRecord
invoice_VAT-name
trainings_auto_cancel
trainings_auto_cancel_threshold
trainings_auto_cancel_deadline] }
trainings_auto_cancel_deadline
trainings_authorization_validity
trainings_authorization_validity_duration
trainings_invalidation_rule
trainings_invalidation_rule_period] }
# WARNING: when adding a new key, you may also want to add it in:
# - config/locales/en.yml#settings
# - app/frontend/src/javascript/models/setting.ts#SettingName
@ -204,13 +208,17 @@ class Setting < ApplicationRecord
end
def previous_value
previous_value = history_values.order(HistoryValue.arel_table['created_at'].desc).limit(2).last
previous_value&.value
last_two = history_values.order(HistoryValue.arel_table['created_at'].desc).limit(2)
return nil if last_two.count < 2
last_two.last&.value
end
def previous_update
previous_value = history_values.order(HistoryValue.arel_table['created_at'].desc).limit(2).last
previous_value&.created_at
last_two = history_values.order(HistoryValue.arel_table['created_at'].desc).limit(2)
return nil if last_two.count < 2
last_two.last&.created_at
end
# @deprecated, prefer Setting.set() instead

View File

@ -23,6 +23,8 @@ class SettingService
validate_admins(settings)
update_accounting_line(settings)
update_trainings_auto_cancel(settings)
update_trainings_authorization(settings)
update_trainings_invalidation(settings)
end
private
@ -108,5 +110,31 @@ class SettingService
Trainings::AutoCancelService.update_auto_cancel(t, tac, threshold, deadline)
end
end
# update trainings authorization parameters
# @param settings [Array<Setting>]
def update_trainings_authorization(settings)
return unless settings.any? { |s| s.name.match(/^trainings_authorization_validity/) }
authorization = settings.find { |s| s.name == 'trainings_authorization_validity' }
duration = settings.find { |s| s.name == 'trainings_authorization_validity_duration' }
Training.find_each do |t|
Trainings::AuthorizationService.update_authorization(t, authorization, duration)
end
end
# update trainings invalidation parameters
# @param settings [Array<Setting>]
def update_trainings_invalidation(settings)
return unless settings.any? { |s| s.name.match(/^trainings_invalidation_rule/) }
invalidation = settings.find { |s| s.name == 'trainings_invalidation_rule' }
duration = settings.find { |s| s.name == 'trainings_invalidation_rule_period' }
Training.find_each do |t|
Trainings::InvalidationService.update_invalidation(t, invalidation, duration)
end
end
end
end

View File

@ -0,0 +1,46 @@
# frozen_string_literal: true
# Business logic around trainings
module Trainings; end
# Automatically cancel trainings authorizations when the configured period has expired
class Trainings::AuthorizationService
class << self
# @param training [Training]
def auto_cancel_authorizations(training)
return unless training.authorization
training.statistic_profile_trainings
.where('created_at < ?', DateTime.current - training.authorization_period.months)
.find_each do |spt|
NotificationCenter.call type: 'notify_member_training_authorization_expired',
receiver: spt.statistic_profile.user,
attached_object: spt.training
spt.destroy!
end
end
# update the given training, depending on the provided settings
# @param training [Training]
# @param authorization [Setting,NilClass]
# @param duration [Setting,NilClass]
def update_authorization(training, authorization, duration)
previous_authorization = if authorization.nil?
Setting.find_by(name: 'trainings_authorization_validity').value
else
authorization.previous_value
end
previous_duration = duration.nil? ? Setting.find_by(name: 'trainings_authorization_validity_duration').value : duration.previous_value
is_default = training.authorization.to_s == previous_authorization.to_s &&
training.authorization_period.to_s == previous_duration.to_s
return unless is_default
# update parameters if the given training is default
params = {}
params[:authorization] = authorization.value unless authorization.nil?
params[:authorization_period] = duration.value unless duration.nil?
training.update(params)
end
end
end

View File

@ -23,6 +23,7 @@ class Trainings::AutoCancelService
attached_object: availability,
meta_data: { auto_refund: auto_refund }
availability.update(lock: true)
availability.slots_reservations.find_each do |sr|
NotificationCenter.call type: 'notify_member_training_auto_cancelled',
receiver: sr.reservation.user,
@ -44,9 +45,9 @@ class Trainings::AutoCancelService
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)
is_default = training.auto_cancel.to_s == previous_auto_cancel.to_s &&
training.auto_cancel_threshold.to_s == previous_threshold.to_s &&
training.auto_cancel_deadline.to_s == previous_deadline.to_s
return unless is_default

View File

@ -0,0 +1,51 @@
# frozen_string_literal: true
# Business logic around trainings
module Trainings; end
# Automatically cancel trainings authorizations if no machines reservations were made during
# the configured period
class Trainings::InvalidationService
class << self
# @param training [Training]
def auto_invalidate(training)
return unless training.invalidation
training.statistic_profile_trainings
.where('created_at < ?', DateTime.current - training.invalidation_period.months)
.find_each do |spt|
reservations_since = spt.statistic_profile
.reservations
.where(reservable_type: 'Machine', reservable_id: spt.training.machines)
.where('created_at > ?', spt.created_at)
.count
next if reservations_since.positive?
NotificationCenter.call type: 'notify_member_training_invalidated',
receiver: spt.statistic_profile.user,
attached_object: spt.training
spt.destroy!
end
end
# update the given training, depending on the provided settings
# @param training [Training]
# @param invalidation [Setting,NilClass]
# @param duration [Setting,NilClass]
def update_invalidation(training, invalidation, duration)
previous_invalidation = invalidation.nil? ? Setting.find_by(name: 'trainings_invalidation_rule').value : invalidation.previous_value
previous_duration = duration.nil? ? Setting.find_by(name: 'trainings_invalidation_rule_period').value : duration.previous_value
is_default = training.invalidation.to_s == previous_invalidation.to_s &&
training.invalidation_period.to_s == previous_duration.to_s
return unless is_default
# update parameters if the given training is default
params = {}
params[:invalidation] = invalidation.value unless invalidation.nil?
params[:invalidation_period] = duration.value unless duration.nil?
training.update(params)
end
end
end

View File

@ -0,0 +1,4 @@
# frozen_string_literal: true
json.title notification.notification_type
json.description t('.training_authorization_revoked', { MACHINES: notification.attached_object.machines.map(&:name).join(', ') })

View File

@ -0,0 +1,4 @@
# frozen_string_literal: true
json.title notification.notification_type
json.description t('.invalidated', { MACHINES: notification.attached_object.machines.map(&:name).join(', ') })

View File

@ -1,7 +1,8 @@
# frozen_string_literal: true
json.extract! training, :id, :name, :description, :machine_ids, :nb_total_places, :public_page, :disabled, :slug,
:auto_cancel, :auto_cancel_threshold, :auto_cancel_deadline
:auto_cancel, :auto_cancel_threshold, :auto_cancel_deadline, :authorization, :authorization_period, :invalidation,
:invalidation_period
if training.training_image
json.training_image_attributes do
json.id training.training_image.id

View File

@ -0,0 +1,8 @@
<%= render 'notifications_mailer/shared/hello', recipient: @recipient %>
<%= t('.body.training_expired_html', {
TRAINING: @attached_object.name,
MACHINES: @attached_object.machines.map(&:name).join(', '),
DATE: I18n.l((DateTime.current - @attached_object.authorization_period.months).to_date),
PERIOD: @attached_object.authorization_period
}) %>

View File

@ -0,0 +1,8 @@
<%= render 'notifications_mailer/shared/hello', recipient: @recipient %>
<%= t('.body.training_invalidated_html', {
TRAINING: @attached_object.name,
MACHINES: @attached_object.machines.map(&:name).join(', '),
DATE: I18n.l((DateTime.current - @attached_object.authorization_period.months).to_date),
PERIOD: @attached_object.authorization_period
}) %>

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
# This training will periodically check for trainings authorizations to revoke
class TrainingAuthorizationWorker
include Sidekiq::Worker
def perform
Training.find_each do |t|
Trainings::AuthorizationService.auto_cancel_authorizations(t)
Trainings::InvalidationService.auto_invalidate(t)
end
end
end

View File

@ -331,9 +331,13 @@ en:
your_subscription_has_expired: "Your subscription has expired."
notify_member_subscription_will_expire_in_7_days:
your_subscription_will_expire_in_7_days: "Your subscription will expire in 7 days."
notify_member_training_authorization_expired:
training_authorization_revoked: "Your authorization to use %{MACHINES} has been revoked because it has expired."
notify_member_training_auto_cancelled:
auto_cancelled_training: "The %{TRAINING} training session scheduled for %{DATE}, has been canceled due to an insufficient number of participants."
auto_refund: "You were refunded on your wallet."
notify_member_training_invalidated:
invalidated: "Your authorization to use %{MACHINES} has been invalidated due to a lack of reservations."
notify_partner_subscribed_plan:
subscription_partner_PLAN_has_been_subscribed_by_USER_html: "Partner subscription <strong><em>%{PLAN}</em></strong> has been subscribed by <strong><em>%{USER}</strong></em>."
notify_project_author_when_collaborator_valid:
@ -675,6 +679,10 @@ en:
trainings_auto_cancel: "Trainings automatic cancellation"
trainings_auto_cancel_threshold: "Minimum participants for automatic cancellation"
trainings_auto_cancel_deadline: "Automatic cancellation deadline"
trainings_authorization_validity: "Trainings validity period"
trainings_authorization_validity_duration: "Trainings validity period duration"
trainings_invalidation_rule: "Trainings automatic invalidation"
trainings_invalidation_rule_period: "Grace period before invalidating a training"
#statuses of projects
statuses:
new: "New"

View File

@ -131,11 +131,19 @@ en:
your_plan: "you plan"
expires_in_7_days: "will expire in 7 days."
to_renew_your_plan_follow_the_link: "Please, follow this link to renew your plan"
notify_member_training_authorization_expired:
subject: "Your authorization was revoked"
body:
training_expired_html: "<p>You took the %{TRAINING} training, on %{DATE}.</p><p>Your authorization for this training, valid for %{PERIOD} months, has expired.</p><p>Please validate it again in order to be able to reserve the %{MACHINES}</p>."
notify_member_training_auto_cancelled:
subject: "Your training session was cancelled"
body:
cancelled_training: "The %{TRAINING} training session scheduled for %{DATE}, from %{START} to %{END} has been canceled due to an insufficient number of participants."
auto_refund: "You were refunded on your wallet and a credit note should be available."
notify_member_training_invalidated:
subject: "Your authorization was invalidated"
body:
training_invalidated_html: "<p>You took the %{TRAINING} training, on %{DATE} giving you access to the %{MACHINES}.</p><p>Due to the lack of reservations for one of these machines during the last %{PERIOD} months, your authorization has been invalidated.</p><p>Please validate the training again in order to continue reserving these machines.</p>."
notify_member_subscription_is_expired:
subject: "Your subscription has expired"
body:

View File

@ -57,4 +57,9 @@ auto_cancel_tranings:
class: TrainingAutoCancelWorker
queue: default
auto_cancel_authorizations:
cron: "0 0 * * *" # every day, at midnight
class: TrainingAuthorizationWorker
queue: default
<%= PluginRegistry.insert_code('yml.schedule') %>

View File

@ -6,7 +6,7 @@
class AddAutoCancelToTrainings < ActiveRecord::Migration[5.2]
def change
change_table :trainings, bulk: true do |t|
t.boolean :auto_cancel, default: false
t.boolean :auto_cancel
t.integer :auto_cancel_threshold
t.integer :auto_cancel_deadline
end

View File

@ -0,0 +1,16 @@
# frozen_string_literal: true
# From this migration, we allows trainings to be valid for a maximum duration, after
# the configured period, the member must validate a new training session.
# Moreover, we allows to configure automatic cancellation of the training validity
# if the member has not used the associated machines for a configurable duration
class AddAuthorizationToTrainings < ActiveRecord::Migration[5.2]
def change
change_table :trainings, bulk: true do |t|
t.boolean :authorization
t.integer :authorization_period
t.boolean :invalidation
t.integer :invalidation_period
end
end
end

View File

@ -19,8 +19,8 @@ ActiveRecord::Schema.define(version: 2023_01_31_104958) do
enable_extension "unaccent"
create_table "abuses", id: :serial, force: :cascade do |t|
t.string "signaled_type"
t.integer "signaled_id"
t.string "signaled_type"
t.string "first_name"
t.string "last_name"
t.string "email"
@ -68,8 +68,8 @@ ActiveRecord::Schema.define(version: 2023_01_31_104958) do
t.string "locality"
t.string "country"
t.string "postal_code"
t.string "placeable_type"
t.integer "placeable_id"
t.string "placeable_type"
t.datetime "created_at"
t.datetime "updated_at"
end
@ -93,8 +93,8 @@ ActiveRecord::Schema.define(version: 2023_01_31_104958) do
end
create_table "assets", id: :serial, force: :cascade do |t|
t.string "viewable_type"
t.integer "viewable_id"
t.string "viewable_type"
t.string "attachment"
t.string "type"
t.datetime "created_at"
@ -281,8 +281,8 @@ ActiveRecord::Schema.define(version: 2023_01_31_104958) do
end
create_table "credits", id: :serial, force: :cascade do |t|
t.string "creditable_type"
t.integer "creditable_id"
t.string "creditable_type"
t.integer "plan_id"
t.integer "hours"
t.datetime "created_at"
@ -524,15 +524,15 @@ ActiveRecord::Schema.define(version: 2023_01_31_104958) do
create_table "notifications", id: :serial, force: :cascade do |t|
t.integer "receiver_id"
t.string "attached_object_type"
t.integer "attached_object_id"
t.string "attached_object_type"
t.integer "notification_type_id"
t.boolean "is_read", default: false
t.datetime "created_at"
t.datetime "updated_at"
t.string "receiver_type"
t.boolean "is_send", default: false
t.jsonb "meta_data", default: "{}"
t.jsonb "meta_data", default: {}
t.index ["notification_type_id"], name: "index_notifications_on_notification_type_id"
t.index ["receiver_id"], name: "index_notifications_on_receiver_id"
end
@ -772,8 +772,8 @@ ActiveRecord::Schema.define(version: 2023_01_31_104958) do
create_table "prices", id: :serial, force: :cascade do |t|
t.integer "group_id"
t.integer "plan_id"
t.string "priceable_type"
t.integer "priceable_id"
t.string "priceable_type"
t.integer "amount"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
@ -976,8 +976,8 @@ ActiveRecord::Schema.define(version: 2023_01_31_104958) do
t.text "message"
t.datetime "created_at"
t.datetime "updated_at"
t.string "reservable_type"
t.integer "reservable_id"
t.string "reservable_type"
t.integer "nb_reserve_places"
t.integer "statistic_profile_id"
t.index ["reservable_type", "reservable_id"], name: "index_reservations_on_reservable_type_and_reservable_id"
@ -986,8 +986,8 @@ ActiveRecord::Schema.define(version: 2023_01_31_104958) do
create_table "roles", id: :serial, force: :cascade do |t|
t.string "name"
t.string "resource_type"
t.integer "resource_id"
t.string "resource_type"
t.datetime "created_at"
t.datetime "updated_at"
t.index ["name", "resource_type", "resource_id"], name: "index_roles_on_name_and_resource_type_and_resource_id"

View File

@ -24,27 +24,3 @@ trainings_machine_5:
training_id: 3
machine_id: 5
trainings_machine_1:
id: 1
training_id: 2
machine_id: 1
trainings_machine_2:
id: 2
training_id: 2
machine_id: 2
trainings_machine_3:
id: 3
training_id: 4
machine_id: 3
trainings_machine_4:
id: 4
training_id: 1
machine_id: 4
trainings_machine_5:
id: 5
training_id: 3
machine_id: 5

View File

@ -72,6 +72,20 @@ class TrainingsTest < ActionDispatch::IntegrationTest
assert_not training[:public_page]
end
test 'user validates a training' do
training = Training.find(3)
user = User.find(9)
put "/api/trainings/#{training.id}", params: { training: {
users: [user.id]
} }.to_json, headers: default_headers
# Check response status
assert_equal 204, response.status, response.body
# Check user is authorized
assert user.training_machine?(Machine.find(5))
end
test 'delete a training' do
delete '/api/trainings/4', headers: default_headers
assert_response :success

View File

@ -0,0 +1,39 @@
# frozen_string_literal: true
require 'test_helper'
class Trainings::AuthorizationServiceTest < ActiveSupport::TestCase
setup do
@training = Training.find(4)
@user = User.find(9)
end
test 'training authorization is revoked after 6 month' do
# Mark training to auto-revoke after 6 month
@training.update(
authorization: true,
authorization_period: 6
)
# User validates a training
StatisticProfileTraining.create!(
statistic_profile_id: @user.statistic_profile.id,
training_id: @training.id
)
# jump to the future and proceed with auto revocations
travel_to(DateTime.current + 6.months + 1.day)
Trainings::AuthorizationService.auto_cancel_authorizations(@training)
# Check authorization was revoked
assert_nil StatisticProfileTraining.find_by(statistic_profile_id: @user.statistic_profile.id, training_id: @training.id)
assert_not @user.training_machine?(Machine.find(3))
# Check notification was sent
notification = Notification.find_by(
notification_type_id: NotificationType.find_by_name('notify_member_training_authorization_expired'), # rubocop:disable Rails/DynamicFindBy
attached_object_type: 'Training',
attached_object_id: @training.id
)
assert_not_nil notification, 'user notification was not created'
end
end

View File

@ -21,6 +21,10 @@ class Trainings::AutoCancelServiceTest < ActiveSupport::TestCase
)
Trainings::AutoCancelService.auto_cancel_reservations(@training)
# Check availability was locked
@availability.reload
assert @availability.lock
# Check reservation was cancelled
r.reload
assert_not_nil r.slots_reservations.first&.canceled_at
@ -77,6 +81,10 @@ class Trainings::AutoCancelServiceTest < ActiveSupport::TestCase
Trainings::AutoCancelService.auto_cancel_reservations(@training)
# Check availability was not locked
@availability.reload
assert_not @availability.lock
# Check nothing was cancelled
r1.reload
assert_nil r1.slots_reservations.first&.canceled_at

View File

@ -0,0 +1,78 @@
# frozen_string_literal: true
require 'test_helper'
class Trainings::InvalidationServiceTest < ActiveSupport::TestCase
setup do
@training = Training.find(4)
@user = User.find(9)
end
test 'training authorization is invalidated after 6 month without reservations' do
# Mark training to invalidable after 6 month
@training.update(
invalidation: true,
invalidation_period: 6
)
# User validates a training
StatisticProfileTraining.create!(
statistic_profile_id: @user.statistic_profile.id,
training_id: @training.id
)
# jump to the future and proceed with auto invalidations
travel_to(DateTime.current + 6.months + 1.day)
Trainings::InvalidationService.auto_invalidate(@training)
# Check authorization was revoked
assert_nil StatisticProfileTraining.find_by(statistic_profile_id: @user.statistic_profile.id, training_id: @training.id)
assert_not @user.training_machine?(Machine.find(3))
# Check notification was sent
notification = Notification.find_by(
notification_type_id: NotificationType.find_by_name('notify_member_training_invalidated'), # rubocop:disable Rails/DynamicFindBy
attached_object_type: 'Training',
attached_object_id: @training.id
)
assert_not_nil notification, 'user notification was not created'
end
test 'training authorization is not invalidated after 6 month with some reservations' do
# Mark training to invalidable after 6 month
@training.update(
invalidation: true,
invalidation_period: 6
)
# User validates a training
StatisticProfileTraining.create!(
statistic_profile_id: @user.statistic_profile.id,
training_id: @training.id
)
# User reserves a machine authorized by this training
machine = @training.machines.first
slot = machine.availabilities.where('start_at > ?', DateTime.current).first&.slots&.first
Reservation.create!(
reservable_id: machine.id,
reservable_type: Machine.name,
slots_reservations_attributes: [{ slot_id: slot&.id }],
statistic_profile_id: @user.statistic_profile.id
)
# jump to the future and proceed with auto invalidations
travel_to(DateTime.current + 6.months + 1.day)
Trainings::InvalidationService.auto_invalidate(@training)
# Check authorization was not revoked
assert_not_nil StatisticProfileTraining.find_by(statistic_profile_id: @user.statistic_profile.id, training_id: @training.id)
assert @user.training_machine?(machine)
# Check notification was not sent
notification = Notification.find_by(
notification_type_id: NotificationType.find_by_name('notify_member_training_invalidated'), # rubocop:disable Rails/DynamicFindBy
attached_object_type: 'Training',
attached_object_id: @training.id
)
assert_nil notification
end
end