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:
parent
78cb8b7854
commit
ddc2dd4c6e
@ -29,7 +29,7 @@ class API::SettingsController < API::ApiController
|
|||||||
updated_settings = []
|
updated_settings = []
|
||||||
may_transaction params[:transactional] do
|
may_transaction params[:transactional] do
|
||||||
params[:settings].each do |setting|
|
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])
|
db_setting = Setting.find_or_initialize_by(name: setting[:name])
|
||||||
if !SettingService.update_allowed?(db_setting)
|
if !SettingService.update_allowed?(db_setting)
|
||||||
|
@ -77,7 +77,8 @@ class API::TrainingsController < API::ApiController
|
|||||||
def training_params
|
def training_params
|
||||||
params.require(:training)
|
params.require(:training)
|
||||||
.permit(:id, :name, :description, :machine_ids, :plan_ids, :nb_total_places, :public_page, :disabled,
|
.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: [],
|
training_image_attributes: %i[id attachment], machine_ids: [], plan_ids: [],
|
||||||
advanced_accounting_attributes: %i[code analytical_section])
|
advanced_accounting_attributes: %i[code analytical_section])
|
||||||
end
|
end
|
||||||
|
@ -39,12 +39,11 @@ export const TrainingForm: React.FC<TrainingFormProps> = ({ action, training, on
|
|||||||
|
|
||||||
const [machineModule, setMachineModule] = useState<Setting>(null);
|
const [machineModule, setMachineModule] = useState<Setting>(null);
|
||||||
const [isActiveAccounting, setIsActiveAccounting] = useState<boolean>(false);
|
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 { handleSubmit, register, control, setValue, formState } = useForm<Training>({ defaultValues: { ...training } });
|
||||||
const output = useWatch<Training>({ control });
|
const output = useWatch<Training>({ control });
|
||||||
const isActiveCancellation = useWatch({ control, name: 'auto_cancel' }) as boolean;
|
const isActiveCancellation = useWatch({ control, name: 'auto_cancel' }) as boolean;
|
||||||
|
const isActiveAuthorizationValidity = useWatch({ control, name: 'authorization' }) as boolean;
|
||||||
|
const isActiveValidationRule = useWatch({ control, name: 'invalidation' });
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
SettingAPI.get('machines_module').then(setMachineModule).catch(onError);
|
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));
|
}).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 (
|
return (
|
||||||
<div className="training-form">
|
<div className="training-form">
|
||||||
<header>
|
<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>
|
<p className="description">{t('app.admin.training_form.authorization_validity_info')}</p>
|
||||||
</header>
|
</header>
|
||||||
<div className="content">
|
<div className="content">
|
||||||
<FormSwitch id="authorization_validity" control={control}
|
<FormSwitch id="authorization" control={control}
|
||||||
onChange={toggleAuthorizationValidity} formState={formState}
|
formState={formState}
|
||||||
defaultValue={isActiveAuthorizationValidity}
|
defaultValue={isActiveAuthorizationValidity}
|
||||||
label={t('app.admin.training_form.authorization_validity_switch')} />
|
label={t('app.admin.training_form.authorization_validity_switch')} />
|
||||||
{isActiveAuthorizationValidity && <>
|
{isActiveAuthorizationValidity && <>
|
||||||
<FormInput id="authorization_validity_duration"
|
<FormInput id="authorization_period"
|
||||||
type="number"
|
type="number"
|
||||||
register={register}
|
register={register}
|
||||||
rules={{ required: isActiveAuthorizationValidity, min: 1 }}
|
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>
|
<p className="description">{t('app.admin.training_form.validation_rule_info')}</p>
|
||||||
</header>
|
</header>
|
||||||
<div className="content">
|
<div className="content">
|
||||||
<FormSwitch id="validation_rule" control={control}
|
<FormSwitch id="invalidation" control={control}
|
||||||
onChange={toggleValidationRule} formState={formState}
|
formState={formState}
|
||||||
defaultValue={isActiveValidationRule}
|
defaultValue={isActiveValidationRule}
|
||||||
label={t('app.admin.training_form.validation_rule_switch')} />
|
label={t('app.admin.training_form.validation_rule_switch')} />
|
||||||
{isActiveValidationRule && <>
|
{isActiveValidationRule && <>
|
||||||
<FormInput id="validation_rule_period"
|
<FormInput id="invalidation_period"
|
||||||
type="number"
|
type="number"
|
||||||
register={register}
|
register={register}
|
||||||
rules={{ required: isActiveValidationRule, min: 1 }}
|
rules={{ required: isActiveValidationRule, min: 1 }}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { IApplication } from '../../models/application';
|
import { IApplication } from '../../models/application';
|
||||||
import { Loader } from '../base/loader';
|
import { Loader } from '../base/loader';
|
||||||
import { react2angular } from 'react2angular';
|
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 { register, control, formState, handleSubmit, reset } = useForm<Record<SettingName, SettingValue>>();
|
||||||
|
|
||||||
const isActiveAutoCancellation = useWatch({ control, name: 'trainings_auto_cancel' }) as boolean;
|
const isActiveAutoCancellation = useWatch({ control, name: 'trainings_auto_cancel' }) as boolean;
|
||||||
const [isActiveAuthorizationValidity, setIsActiveAuthorizationValidity] = useState<boolean>(false);
|
const isActiveAuthorizationValidity = useWatch({ control, name: 'trainings_authorization_validity' }) as boolean;
|
||||||
const [isActiveValidationRule, setIsActiveValidationRule] = useState<boolean>(false);
|
const isActiveInvalidationRule = useWatch({ control, name: 'trainings_invalidation_rule' }) as boolean;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
SettingAPI.query(trainingSettings)
|
SettingAPI.query(trainingSettings)
|
||||||
@ -41,20 +41,6 @@ export const TrainingsSettings: React.FC<TrainingsSettingsProps> = ({ onError, o
|
|||||||
.catch(onError);
|
.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
|
* 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>
|
<p className="description">{t('app.admin.trainings_settings.authorization_validity_info')}</p>
|
||||||
</header>
|
</header>
|
||||||
<div className="content">
|
<div className="content">
|
||||||
<FormSwitch id="authorization_validity" control={control}
|
<FormSwitch id="trainings_authorization_validity" control={control}
|
||||||
onChange={toggleAuthorizationValidity} formState={formState}
|
formState={formState}
|
||||||
defaultValue={isActiveAuthorizationValidity}
|
defaultValue={isActiveAuthorizationValidity}
|
||||||
label={t('app.admin.trainings_settings.authorization_validity_switch')} />
|
label={t('app.admin.trainings_settings.authorization_validity_switch')} />
|
||||||
{isActiveAuthorizationValidity && <>
|
{isActiveAuthorizationValidity && <>
|
||||||
<FormInput id="authorization_validity_duration"
|
<FormInput id="trainings_authorization_validity_duration"
|
||||||
type="number"
|
type="number"
|
||||||
register={register}
|
register={register}
|
||||||
rules={{ required: isActiveAuthorizationValidity, min: 1 }}
|
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>
|
<p className="description">{t('app.admin.trainings_settings.validation_rule_info')}</p>
|
||||||
</header>
|
</header>
|
||||||
<div className="content">
|
<div className="content">
|
||||||
<FormSwitch id="validation_rule" control={control}
|
<FormSwitch id="trainings_invalidation_rule" control={control}
|
||||||
onChange={toggleValidationRule} formState={formState}
|
formState={formState}
|
||||||
defaultValue={isActiveValidationRule}
|
defaultValue={isActiveInvalidationRule}
|
||||||
label={t('app.admin.trainings_settings.validation_rule_switch')} />
|
label={t('app.admin.trainings_settings.validation_rule_switch')} />
|
||||||
{isActiveValidationRule && <>
|
{isActiveInvalidationRule && <>
|
||||||
<FormInput id="validation_rule_period"
|
<FormInput id="trainings_invalidation_rule_period"
|
||||||
type="number"
|
type="number"
|
||||||
register={register}
|
register={register}
|
||||||
rules={{ required: isActiveValidationRule, min: 1 }}
|
rules={{ required: isActiveInvalidationRule, min: 1 }}
|
||||||
step={1}
|
step={1}
|
||||||
formState={formState}
|
formState={formState}
|
||||||
label={t('app.admin.trainings_settings.validation_rule_period')} />
|
label={t('app.admin.trainings_settings.validation_rule_period')} />
|
||||||
|
@ -159,16 +159,16 @@ export const Trainings: React.FC<TrainingsProps> = ({ onError, onSuccess }) => {
|
|||||||
|
|
||||||
<div className='authorisation'>
|
<div className='authorisation'>
|
||||||
<span>{t('app.admin.trainings.authorisation')}</span>
|
<span>{t('app.admin.trainings.authorisation')}</span>
|
||||||
<p>
|
{(training.authorization && <p>
|
||||||
{t('app.admin.trainings.active_true')}
|
{t('app.admin.trainings.active_true')}
|
||||||
<span>|</span>{t('app.admin.trainings.period_MONTH', { MONTH: 48 })}
|
<span>|</span>{t('app.admin.trainings.period_MONTH', { MONTH: training.authorization_period })}
|
||||||
</p>
|
</p>) || <p>---</p>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='rule'>
|
<div className='rule'>
|
||||||
<span>{t('app.admin.trainings.validation_rule')}</span>
|
<span>{t('app.admin.trainings.validation_rule')}</span>
|
||||||
<p>
|
<p>
|
||||||
{t('app.admin.trainings.active_false')}
|
{training.invalidation ? t('app.admin.trainings.active_true') : t('app.admin.trainings.active_false')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -236,7 +236,11 @@ export const storeSettings = [
|
|||||||
export const trainingSettings = [
|
export const trainingSettings = [
|
||||||
'trainings_auto_cancel',
|
'trainings_auto_cancel',
|
||||||
'trainings_auto_cancel_threshold',
|
'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;
|
] as const;
|
||||||
|
|
||||||
export const allSettings = [
|
export const allSettings = [
|
||||||
|
@ -17,6 +17,10 @@ export interface Training {
|
|||||||
auto_cancel: boolean,
|
auto_cancel: boolean,
|
||||||
auto_cancel_threshold: number,
|
auto_cancel_threshold: number,
|
||||||
auto_cancel_deadline: number,
|
auto_cancel_deadline: number,
|
||||||
|
authorization: boolean,
|
||||||
|
authorization_period: number,
|
||||||
|
invalidation: boolean,
|
||||||
|
invalidation_period: number,
|
||||||
availabilities?: Array<{
|
availabilities?: Array<{
|
||||||
id: number,
|
id: number,
|
||||||
start_at: TDateISO,
|
start_at: TDateISO,
|
||||||
@ -33,5 +37,5 @@ export interface Training {
|
|||||||
export interface TrainingIndexFilter extends ApiFilter {
|
export interface TrainingIndexFilter extends ApiFilter {
|
||||||
disabled?: boolean,
|
disabled?: boolean,
|
||||||
public_page?: boolean,
|
public_page?: boolean,
|
||||||
requested_attributes?: ['availabillities'],
|
requested_attributes?: ['availabilities'],
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# 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
|
module UserRessourcesConcern
|
||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
@ -75,6 +75,8 @@ class NotificationType
|
|||||||
notify_admin_low_stock_threshold
|
notify_admin_low_stock_threshold
|
||||||
notify_admin_training_auto_cancelled
|
notify_admin_training_auto_cancelled
|
||||||
notify_member_training_auto_cancelled
|
notify_member_training_auto_cancelled
|
||||||
|
notify_member_training_authorization_expired
|
||||||
|
notify_member_training_invalidated
|
||||||
]
|
]
|
||||||
# deprecated:
|
# deprecated:
|
||||||
# - notify_member_subscribed_plan_is_changed
|
# - notify_member_subscribed_plan_is_changed
|
||||||
|
@ -168,7 +168,11 @@ class Setting < ApplicationRecord
|
|||||||
invoice_VAT-name
|
invoice_VAT-name
|
||||||
trainings_auto_cancel
|
trainings_auto_cancel
|
||||||
trainings_auto_cancel_threshold
|
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:
|
# WARNING: when adding a new key, you may also want to add it in:
|
||||||
# - config/locales/en.yml#settings
|
# - config/locales/en.yml#settings
|
||||||
# - app/frontend/src/javascript/models/setting.ts#SettingName
|
# - app/frontend/src/javascript/models/setting.ts#SettingName
|
||||||
@ -204,13 +208,17 @@ class Setting < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def previous_value
|
def previous_value
|
||||||
previous_value = history_values.order(HistoryValue.arel_table['created_at'].desc).limit(2).last
|
last_two = history_values.order(HistoryValue.arel_table['created_at'].desc).limit(2)
|
||||||
previous_value&.value
|
return nil if last_two.count < 2
|
||||||
|
|
||||||
|
last_two.last&.value
|
||||||
end
|
end
|
||||||
|
|
||||||
def previous_update
|
def previous_update
|
||||||
previous_value = history_values.order(HistoryValue.arel_table['created_at'].desc).limit(2).last
|
last_two = history_values.order(HistoryValue.arel_table['created_at'].desc).limit(2)
|
||||||
previous_value&.created_at
|
return nil if last_two.count < 2
|
||||||
|
|
||||||
|
last_two.last&.created_at
|
||||||
end
|
end
|
||||||
|
|
||||||
# @deprecated, prefer Setting.set() instead
|
# @deprecated, prefer Setting.set() instead
|
||||||
|
@ -23,6 +23,8 @@ class SettingService
|
|||||||
validate_admins(settings)
|
validate_admins(settings)
|
||||||
update_accounting_line(settings)
|
update_accounting_line(settings)
|
||||||
update_trainings_auto_cancel(settings)
|
update_trainings_auto_cancel(settings)
|
||||||
|
update_trainings_authorization(settings)
|
||||||
|
update_trainings_invalidation(settings)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
@ -108,5 +110,31 @@ class SettingService
|
|||||||
Trainings::AutoCancelService.update_auto_cancel(t, tac, threshold, deadline)
|
Trainings::AutoCancelService.update_auto_cancel(t, tac, threshold, deadline)
|
||||||
end
|
end
|
||||||
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
|
||||||
end
|
end
|
||||||
|
46
app/services/trainings/authorization_service.rb
Normal file
46
app/services/trainings/authorization_service.rb
Normal 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
|
@ -23,6 +23,7 @@ class Trainings::AutoCancelService
|
|||||||
attached_object: availability,
|
attached_object: availability,
|
||||||
meta_data: { auto_refund: auto_refund }
|
meta_data: { auto_refund: auto_refund }
|
||||||
|
|
||||||
|
availability.update(lock: true)
|
||||||
availability.slots_reservations.find_each do |sr|
|
availability.slots_reservations.find_each do |sr|
|
||||||
NotificationCenter.call type: 'notify_member_training_auto_cancelled',
|
NotificationCenter.call type: 'notify_member_training_auto_cancelled',
|
||||||
receiver: sr.reservation.user,
|
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_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_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
|
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 &&
|
is_default = training.auto_cancel.to_s == previous_auto_cancel.to_s &&
|
||||||
[nil, previous_threshold].include?(training.auto_cancel_threshold.to_s) &&
|
training.auto_cancel_threshold.to_s == previous_threshold.to_s &&
|
||||||
[nil, previous_deadline].include?(training.auto_cancel_deadline.to_s)
|
training.auto_cancel_deadline.to_s == previous_deadline.to_s
|
||||||
|
|
||||||
return unless is_default
|
return unless is_default
|
||||||
|
|
||||||
|
51
app/services/trainings/invalidation_service.rb
Normal file
51
app/services/trainings/invalidation_service.rb
Normal 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
|
@ -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(', ') })
|
@ -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(', ') })
|
@ -1,7 +1,8 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
json.extract! training, :id, :name, :description, :machine_ids, :nb_total_places, :public_page, :disabled, :slug,
|
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
|
if training.training_image
|
||||||
json.training_image_attributes do
|
json.training_image_attributes do
|
||||||
json.id training.training_image.id
|
json.id training.training_image.id
|
||||||
|
@ -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
|
||||||
|
}) %>
|
@ -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
|
||||||
|
}) %>
|
13
app/workers/training_authorization_worker.rb
Normal file
13
app/workers/training_authorization_worker.rb
Normal 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
|
@ -331,9 +331,13 @@ en:
|
|||||||
your_subscription_has_expired: "Your subscription has expired."
|
your_subscription_has_expired: "Your subscription has expired."
|
||||||
notify_member_subscription_will_expire_in_7_days:
|
notify_member_subscription_will_expire_in_7_days:
|
||||||
your_subscription_will_expire_in_7_days: "Your 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:
|
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_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."
|
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:
|
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>."
|
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:
|
notify_project_author_when_collaborator_valid:
|
||||||
@ -675,6 +679,10 @@ en:
|
|||||||
trainings_auto_cancel: "Trainings automatic cancellation"
|
trainings_auto_cancel: "Trainings automatic cancellation"
|
||||||
trainings_auto_cancel_threshold: "Minimum participants for automatic cancellation"
|
trainings_auto_cancel_threshold: "Minimum participants for automatic cancellation"
|
||||||
trainings_auto_cancel_deadline: "Automatic cancellation deadline"
|
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 of projects
|
||||||
statuses:
|
statuses:
|
||||||
new: "New"
|
new: "New"
|
||||||
|
@ -131,11 +131,19 @@ en:
|
|||||||
your_plan: "you plan"
|
your_plan: "you plan"
|
||||||
expires_in_7_days: "will expire in 7 days."
|
expires_in_7_days: "will expire in 7 days."
|
||||||
to_renew_your_plan_follow_the_link: "Please, follow this link to renew your plan"
|
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:
|
notify_member_training_auto_cancelled:
|
||||||
subject: "Your training session was cancelled"
|
subject: "Your training session was cancelled"
|
||||||
body:
|
body:
|
||||||
cancelled_training: "The %{TRAINING} training session scheduled for %{DATE}, from %{START} to %{END} has been canceled due to an insufficient number of participants."
|
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."
|
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:
|
notify_member_subscription_is_expired:
|
||||||
subject: "Your subscription has expired"
|
subject: "Your subscription has expired"
|
||||||
body:
|
body:
|
||||||
|
@ -57,4 +57,9 @@ auto_cancel_tranings:
|
|||||||
class: TrainingAutoCancelWorker
|
class: TrainingAutoCancelWorker
|
||||||
queue: default
|
queue: default
|
||||||
|
|
||||||
|
auto_cancel_authorizations:
|
||||||
|
cron: "0 0 * * *" # every day, at midnight
|
||||||
|
class: TrainingAuthorizationWorker
|
||||||
|
queue: default
|
||||||
|
|
||||||
<%= PluginRegistry.insert_code('yml.schedule') %>
|
<%= PluginRegistry.insert_code('yml.schedule') %>
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
class AddAutoCancelToTrainings < ActiveRecord::Migration[5.2]
|
class AddAutoCancelToTrainings < ActiveRecord::Migration[5.2]
|
||||||
def change
|
def change
|
||||||
change_table :trainings, bulk: true do |t|
|
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_threshold
|
||||||
t.integer :auto_cancel_deadline
|
t.integer :auto_cancel_deadline
|
||||||
end
|
end
|
||||||
|
16
db/migrate/20230127091337_add_authorization_to_trainings.rb
Normal file
16
db/migrate/20230127091337_add_authorization_to_trainings.rb
Normal 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
|
18
db/schema.rb
18
db/schema.rb
@ -19,8 +19,8 @@ ActiveRecord::Schema.define(version: 2023_01_31_104958) do
|
|||||||
enable_extension "unaccent"
|
enable_extension "unaccent"
|
||||||
|
|
||||||
create_table "abuses", id: :serial, force: :cascade do |t|
|
create_table "abuses", id: :serial, force: :cascade do |t|
|
||||||
t.string "signaled_type"
|
|
||||||
t.integer "signaled_id"
|
t.integer "signaled_id"
|
||||||
|
t.string "signaled_type"
|
||||||
t.string "first_name"
|
t.string "first_name"
|
||||||
t.string "last_name"
|
t.string "last_name"
|
||||||
t.string "email"
|
t.string "email"
|
||||||
@ -68,8 +68,8 @@ ActiveRecord::Schema.define(version: 2023_01_31_104958) do
|
|||||||
t.string "locality"
|
t.string "locality"
|
||||||
t.string "country"
|
t.string "country"
|
||||||
t.string "postal_code"
|
t.string "postal_code"
|
||||||
t.string "placeable_type"
|
|
||||||
t.integer "placeable_id"
|
t.integer "placeable_id"
|
||||||
|
t.string "placeable_type"
|
||||||
t.datetime "created_at"
|
t.datetime "created_at"
|
||||||
t.datetime "updated_at"
|
t.datetime "updated_at"
|
||||||
end
|
end
|
||||||
@ -93,8 +93,8 @@ ActiveRecord::Schema.define(version: 2023_01_31_104958) do
|
|||||||
end
|
end
|
||||||
|
|
||||||
create_table "assets", id: :serial, force: :cascade do |t|
|
create_table "assets", id: :serial, force: :cascade do |t|
|
||||||
t.string "viewable_type"
|
|
||||||
t.integer "viewable_id"
|
t.integer "viewable_id"
|
||||||
|
t.string "viewable_type"
|
||||||
t.string "attachment"
|
t.string "attachment"
|
||||||
t.string "type"
|
t.string "type"
|
||||||
t.datetime "created_at"
|
t.datetime "created_at"
|
||||||
@ -281,8 +281,8 @@ ActiveRecord::Schema.define(version: 2023_01_31_104958) do
|
|||||||
end
|
end
|
||||||
|
|
||||||
create_table "credits", id: :serial, force: :cascade do |t|
|
create_table "credits", id: :serial, force: :cascade do |t|
|
||||||
t.string "creditable_type"
|
|
||||||
t.integer "creditable_id"
|
t.integer "creditable_id"
|
||||||
|
t.string "creditable_type"
|
||||||
t.integer "plan_id"
|
t.integer "plan_id"
|
||||||
t.integer "hours"
|
t.integer "hours"
|
||||||
t.datetime "created_at"
|
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|
|
create_table "notifications", id: :serial, force: :cascade do |t|
|
||||||
t.integer "receiver_id"
|
t.integer "receiver_id"
|
||||||
t.string "attached_object_type"
|
|
||||||
t.integer "attached_object_id"
|
t.integer "attached_object_id"
|
||||||
|
t.string "attached_object_type"
|
||||||
t.integer "notification_type_id"
|
t.integer "notification_type_id"
|
||||||
t.boolean "is_read", default: false
|
t.boolean "is_read", default: false
|
||||||
t.datetime "created_at"
|
t.datetime "created_at"
|
||||||
t.datetime "updated_at"
|
t.datetime "updated_at"
|
||||||
t.string "receiver_type"
|
t.string "receiver_type"
|
||||||
t.boolean "is_send", default: false
|
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 ["notification_type_id"], name: "index_notifications_on_notification_type_id"
|
||||||
t.index ["receiver_id"], name: "index_notifications_on_receiver_id"
|
t.index ["receiver_id"], name: "index_notifications_on_receiver_id"
|
||||||
end
|
end
|
||||||
@ -772,8 +772,8 @@ ActiveRecord::Schema.define(version: 2023_01_31_104958) do
|
|||||||
create_table "prices", id: :serial, force: :cascade do |t|
|
create_table "prices", id: :serial, force: :cascade do |t|
|
||||||
t.integer "group_id"
|
t.integer "group_id"
|
||||||
t.integer "plan_id"
|
t.integer "plan_id"
|
||||||
t.string "priceable_type"
|
|
||||||
t.integer "priceable_id"
|
t.integer "priceable_id"
|
||||||
|
t.string "priceable_type"
|
||||||
t.integer "amount"
|
t.integer "amount"
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_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.text "message"
|
||||||
t.datetime "created_at"
|
t.datetime "created_at"
|
||||||
t.datetime "updated_at"
|
t.datetime "updated_at"
|
||||||
t.string "reservable_type"
|
|
||||||
t.integer "reservable_id"
|
t.integer "reservable_id"
|
||||||
|
t.string "reservable_type"
|
||||||
t.integer "nb_reserve_places"
|
t.integer "nb_reserve_places"
|
||||||
t.integer "statistic_profile_id"
|
t.integer "statistic_profile_id"
|
||||||
t.index ["reservable_type", "reservable_id"], name: "index_reservations_on_reservable_type_and_reservable_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|
|
create_table "roles", id: :serial, force: :cascade do |t|
|
||||||
t.string "name"
|
t.string "name"
|
||||||
t.string "resource_type"
|
|
||||||
t.integer "resource_id"
|
t.integer "resource_id"
|
||||||
|
t.string "resource_type"
|
||||||
t.datetime "created_at"
|
t.datetime "created_at"
|
||||||
t.datetime "updated_at"
|
t.datetime "updated_at"
|
||||||
t.index ["name", "resource_type", "resource_id"], name: "index_roles_on_name_and_resource_type_and_resource_id"
|
t.index ["name", "resource_type", "resource_id"], name: "index_roles_on_name_and_resource_type_and_resource_id"
|
||||||
|
24
test/fixtures/trainings_machines.yml
vendored
24
test/fixtures/trainings_machines.yml
vendored
@ -24,27 +24,3 @@ trainings_machine_5:
|
|||||||
training_id: 3
|
training_id: 3
|
||||||
machine_id: 5
|
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
|
|
||||||
|
@ -72,6 +72,20 @@ class TrainingsTest < ActionDispatch::IntegrationTest
|
|||||||
assert_not training[:public_page]
|
assert_not training[:public_page]
|
||||||
end
|
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
|
test 'delete a training' do
|
||||||
delete '/api/trainings/4', headers: default_headers
|
delete '/api/trainings/4', headers: default_headers
|
||||||
assert_response :success
|
assert_response :success
|
||||||
|
39
test/services/trainings/authorization_service_test.rb
Normal file
39
test/services/trainings/authorization_service_test.rb
Normal 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
|
@ -21,6 +21,10 @@ class Trainings::AutoCancelServiceTest < ActiveSupport::TestCase
|
|||||||
)
|
)
|
||||||
Trainings::AutoCancelService.auto_cancel_reservations(@training)
|
Trainings::AutoCancelService.auto_cancel_reservations(@training)
|
||||||
|
|
||||||
|
# Check availability was locked
|
||||||
|
@availability.reload
|
||||||
|
assert @availability.lock
|
||||||
|
|
||||||
# Check reservation was cancelled
|
# Check reservation was cancelled
|
||||||
r.reload
|
r.reload
|
||||||
assert_not_nil r.slots_reservations.first&.canceled_at
|
assert_not_nil r.slots_reservations.first&.canceled_at
|
||||||
@ -77,6 +81,10 @@ class Trainings::AutoCancelServiceTest < ActiveSupport::TestCase
|
|||||||
|
|
||||||
Trainings::AutoCancelService.auto_cancel_reservations(@training)
|
Trainings::AutoCancelService.auto_cancel_reservations(@training)
|
||||||
|
|
||||||
|
# Check availability was not locked
|
||||||
|
@availability.reload
|
||||||
|
assert_not @availability.lock
|
||||||
|
|
||||||
# Check nothing was cancelled
|
# Check nothing was cancelled
|
||||||
r1.reload
|
r1.reload
|
||||||
assert_nil r1.slots_reservations.first&.canceled_at
|
assert_nil r1.slots_reservations.first&.canceled_at
|
||||||
|
78
test/services/trainings/invalidation_service_test.rb
Normal file
78
test/services/trainings/invalidation_service_test.rb
Normal 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
|
Loading…
x
Reference in New Issue
Block a user