Service that computes administrator statistics and sends them to SendinBlue
This commit is contained in:
parent
98713b6a4d
commit
6b0832047b
2 changed files with 384 additions and 0 deletions
166
app/services/administrateur_usage_statistics_service.rb
Normal file
166
app/services/administrateur_usage_statistics_service.rb
Normal file
|
@ -0,0 +1,166 @@
|
|||
# Note: this class uses a `synthetic_state` for Dossier, that diverges from the standard state:
|
||||
# - 'termine' is the synthetic_state for all dossiers
|
||||
# whose state is 'accepte', 'refuse' or 'sans_suite',
|
||||
# even when `archive` is true
|
||||
# - 'archive' is the synthetic_state for all dossiers
|
||||
# where archive is true,
|
||||
# except those whose synthetic_state is already 'termine'
|
||||
# - For all other dossiers, the synthetic_state and the state are the same
|
||||
class AdministrateurUsageStatisticsService
|
||||
def update_administrateurs
|
||||
Administrateur.find_each do |administrateur|
|
||||
stats = administrateur_stats(administrateur)
|
||||
api.identify(administrateur.email, stats)
|
||||
end
|
||||
api.run
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def api
|
||||
@api ||= Sendinblue::Api.new_properly_configured!
|
||||
end
|
||||
|
||||
def administrateur_stats(administrateur)
|
||||
nb_dossiers_by_procedure_id = nb_dossiers_by_procedure_id(administrateur.id)
|
||||
nb_dossiers_by_synthetic_state = nb_dossiers_by_synthetic_state(administrateur.id)
|
||||
|
||||
result = {
|
||||
ds_sign_in_count: administrateur.sign_in_count,
|
||||
ds_created_at: administrateur.created_at,
|
||||
ds_active: administrateur.active,
|
||||
ds_id: administrateur.id,
|
||||
ds_features: administrateur.features.to_json,
|
||||
nb_services: nb_services_by_administrateur_id[administrateur.id],
|
||||
nb_instructeurs: nb_instructeurs_by_administrateur_id[administrateur.id],
|
||||
|
||||
ds_nb_demarches_actives: nb_demarches_by_administrateur_id_and_state[[administrateur.id, "publiee"]],
|
||||
ds_nb_demarches_archives: nb_demarches_by_administrateur_id_and_state[[administrateur.id, "archivee"]],
|
||||
ds_nb_demarches_brouillons: nb_demarches_by_administrateur_id_and_state[[administrateur.id, "brouillon"]],
|
||||
|
||||
nb_demarches_test: nb_dossiers_by_procedure_id
|
||||
.select { |procedure_id, count| count > 0 && is_brouillon(procedure_id) }
|
||||
.count,
|
||||
nb_demarches_prod: nb_dossiers_by_procedure_id
|
||||
.reject { |procedure_id, count| count == 0 || is_brouillon(procedure_id) }
|
||||
.count,
|
||||
nb_demarches_prod_20: nb_dossiers_by_procedure_id
|
||||
.reject { |procedure_id, count| count < 20 || is_brouillon(procedure_id) }
|
||||
.count,
|
||||
|
||||
nb_dossiers: nb_dossiers_by_procedure_id
|
||||
.reject { |procedure_id, _count| is_brouillon(procedure_id) }
|
||||
.map { |_procedure_id, count| count }
|
||||
.sum,
|
||||
nb_dossiers_max: nb_dossiers_by_procedure_id
|
||||
.reject { |procedure_id, _count| is_brouillon(procedure_id) }
|
||||
.map { |_procedure_id, count| count }
|
||||
.max || 0,
|
||||
nb_dossiers_traite: nb_dossiers_by_synthetic_state['termine'],
|
||||
nb_dossiers_dossier_en_instruction: nb_dossiers_by_synthetic_state['en_instruction']
|
||||
}
|
||||
|
||||
if administrateur.current_sign_in_at.present?
|
||||
result[:ds_current_sign_in_at] = administrateur.current_sign_in_at
|
||||
end
|
||||
|
||||
if administrateur.last_sign_in_at.present?
|
||||
result[:ds_last_sign_in_at] = administrateur.last_sign_in_at
|
||||
end
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
# Returns a hash { procedure_id => dossier_count }:
|
||||
# - The keys are the ids of procedures owned by administrateur_id
|
||||
# - The values are the number of dossiers for that procedure.
|
||||
# Brouillons, and dossiers that are 'archive' but not 'termine', are not counted.
|
||||
def nb_dossiers_by_procedure_id(administrateur_id)
|
||||
with_default(
|
||||
0,
|
||||
nb_dossiers_by_administrateur_id_and_procedure_id_and_synthetic_state[administrateur_id]
|
||||
.map do |procedure_id, nb_dossiers_by_synthetic_state|
|
||||
[
|
||||
procedure_id,
|
||||
nb_dossiers_by_synthetic_state
|
||||
.reject { |synthetic_state, _count| ['brouillon', 'archive'].include?(synthetic_state) }
|
||||
.map { |_synthetic_state, count| count }
|
||||
.sum
|
||||
]
|
||||
end
|
||||
.to_h
|
||||
)
|
||||
end
|
||||
|
||||
# Returns a hash { synthetic_state => dossier_count }
|
||||
# - The keys are dossier synthetic_states (see class comment)
|
||||
# - The values are the number of dossiers in that synthetic state, for procedures owned by `administrateur_id`
|
||||
# Dossier on procedures en test are not counted
|
||||
def nb_dossiers_by_synthetic_state(administrateur_id)
|
||||
with_default(
|
||||
0,
|
||||
nb_dossiers_by_administrateur_id_and_procedure_id_and_synthetic_state[administrateur_id]
|
||||
.reject { |procedure_id, _nb_dossiers_by_synthetic_state| is_brouillon(procedure_id) }
|
||||
.flat_map { |_procedure_id, nb_dossiers_by_synthetic_state| nb_dossiers_by_synthetic_state.to_a }
|
||||
.group_by { |synthetic_state, _count| synthetic_state }
|
||||
.map { |synthetic_state, synthetic_states_and_counts| [synthetic_state, synthetic_states_and_counts.map { |_synthetic_state, count| count }.sum] }
|
||||
.to_h
|
||||
)
|
||||
end
|
||||
|
||||
def nb_demarches_by_administrateur_id_and_state
|
||||
@nb_demarches_by_administrateur_id_and_state ||= with_default(0, Procedure.group(:administrateur_id, :aasm_state).count)
|
||||
end
|
||||
|
||||
def nb_services_by_administrateur_id
|
||||
@nb_services_by_administrateur_id ||= with_default(0, Service.group(:administrateur_id).count)
|
||||
end
|
||||
|
||||
def nb_instructeurs_by_administrateur_id
|
||||
@nb_instructeurs_by_administrateur_id ||= with_default(0, Administrateur.joins(:gestionnaires).group(:administrateur_id).count)
|
||||
end
|
||||
|
||||
def nb_dossiers_by_administrateur_id_and_procedure_id_and_synthetic_state
|
||||
if @nb_dossiers_by_administrateur_id_and_procedure_id_and_synthetic_state.present?
|
||||
return @nb_dossiers_by_administrateur_id_and_procedure_id_and_synthetic_state
|
||||
end
|
||||
|
||||
result = {}
|
||||
|
||||
Dossier
|
||||
.joins(:procedure)
|
||||
.group(
|
||||
:administrateur_id,
|
||||
:procedure_id,
|
||||
<<~EOSQL
|
||||
CASE
|
||||
WHEN state IN('accepte', 'refuse', 'sans_suite') THEN 'termine'
|
||||
WHEN archived THEN 'archive'
|
||||
ELSE state
|
||||
END
|
||||
EOSQL
|
||||
)
|
||||
.count
|
||||
.each do |(administrateur_id, procedure_id, synthetic_state), count|
|
||||
result.deep_merge!(
|
||||
{ administrateur_id => { procedure_id => { synthetic_state => count } } }
|
||||
)
|
||||
end
|
||||
|
||||
@nb_dossiers_by_administrateur_id_and_procedure_id_and_synthetic_state =
|
||||
with_default({}, result)
|
||||
end
|
||||
|
||||
def is_brouillon(procedure_id)
|
||||
procedure_states[procedure_id] == 'brouillon'
|
||||
end
|
||||
|
||||
def procedure_states
|
||||
@procedure_states ||= Procedure.pluck(:id, :aasm_state).to_h
|
||||
end
|
||||
|
||||
def with_default(default, hash)
|
||||
hash.default = default
|
||||
hash
|
||||
end
|
||||
end
|
218
spec/services/administrateur_usage_statistics_service_spec.rb
Normal file
218
spec/services/administrateur_usage_statistics_service_spec.rb
Normal file
|
@ -0,0 +1,218 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe AdministrateurUsageStatisticsService do
|
||||
describe '#administrateur_stats' do
|
||||
let(:service) { AdministrateurUsageStatisticsService.new }
|
||||
subject { service.send(:administrateur_stats, administrateur) }
|
||||
|
||||
before { Timecop.freeze(Time.zone.now) }
|
||||
after { Timecop.return }
|
||||
|
||||
context 'for an administrateur that has nothing' do
|
||||
let(:administrateur) { create(:administrateur) }
|
||||
|
||||
it do
|
||||
is_expected.to eq(
|
||||
ds_sign_in_count: 0,
|
||||
ds_created_at: Time.zone.now,
|
||||
ds_active: false,
|
||||
ds_id: administrateur.id,
|
||||
ds_features: "{}",
|
||||
nb_services: 0,
|
||||
nb_instructeurs: 0,
|
||||
ds_nb_demarches_actives: 0,
|
||||
ds_nb_demarches_archives: 0,
|
||||
ds_nb_demarches_brouillons: 0,
|
||||
nb_demarches_test: 0,
|
||||
nb_demarches_prod: 0,
|
||||
nb_demarches_prod_20: 0,
|
||||
nb_dossiers: 0,
|
||||
nb_dossiers_max: 0,
|
||||
nb_dossiers_traite: 0,
|
||||
nb_dossiers_dossier_en_instruction: 0
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'for an administrateur that has plenty of things' do
|
||||
let(:administrateur) do
|
||||
create(:administrateur,
|
||||
sign_in_count: 17,
|
||||
current_sign_in_at: Time.zone.local(2019, 3, 7),
|
||||
last_sign_in_at: Time.zone.local(2019, 2, 27),
|
||||
active: true,
|
||||
features: { holy_hand_grenade_of_antioch: true },
|
||||
services: [create(:service)],
|
||||
gestionnaires: [create(:gestionnaire)])
|
||||
end
|
||||
|
||||
it do
|
||||
is_expected.to include(
|
||||
ds_sign_in_count: 17,
|
||||
ds_current_sign_in_at: Time.zone.local(2019, 3, 7),
|
||||
ds_last_sign_in_at: Time.zone.local(2019, 2, 27),
|
||||
ds_created_at: Time.zone.now,
|
||||
ds_active: true,
|
||||
ds_id: administrateur.id,
|
||||
ds_features: { holy_hand_grenade_of_antioch: true }.to_json,
|
||||
nb_services: 1,
|
||||
nb_instructeurs: 1
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'counting procedures and dossiers' do
|
||||
let(:administrateur) do
|
||||
create(:administrateur, procedures: [procedure])
|
||||
end
|
||||
|
||||
context 'with a freshly active procedure' do
|
||||
let(:procedure) { create(:procedure, aasm_state: 'publiee') }
|
||||
|
||||
it do
|
||||
is_expected.to include(
|
||||
ds_nb_demarches_actives: 1,
|
||||
ds_nb_demarches_archives: 0,
|
||||
ds_nb_demarches_brouillons: 0,
|
||||
nb_demarches_test: 0,
|
||||
nb_demarches_prod: 0,
|
||||
nb_demarches_prod_20: 0,
|
||||
nb_dossiers: 0,
|
||||
nb_dossiers_max: 0,
|
||||
nb_dossiers_traite: 0,
|
||||
nb_dossiers_dossier_en_instruction: 0
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a procedure archivee' do
|
||||
let(:procedure) { create(:procedure, aasm_state: 'archivee', dossiers: dossiers) }
|
||||
let(:dossiers) do
|
||||
(1..7).flat_map do
|
||||
[
|
||||
create(:dossier, :en_construction),
|
||||
create(:dossier, :en_instruction),
|
||||
create(:dossier, :accepte)
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
it do
|
||||
is_expected.to include(
|
||||
ds_nb_demarches_actives: 0,
|
||||
ds_nb_demarches_archives: 1,
|
||||
ds_nb_demarches_brouillons: 0,
|
||||
nb_demarches_test: 0,
|
||||
nb_demarches_prod: 1,
|
||||
nb_demarches_prod_20: 1,
|
||||
nb_dossiers: 21,
|
||||
nb_dossiers_max: 21,
|
||||
nb_dossiers_traite: 7,
|
||||
nb_dossiers_dossier_en_instruction: 7
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a procedure brouillon' do
|
||||
let(:procedure) { create(:procedure) }
|
||||
|
||||
it do
|
||||
is_expected.to include(
|
||||
ds_nb_demarches_actives: 0,
|
||||
ds_nb_demarches_archives: 0,
|
||||
ds_nb_demarches_brouillons: 1,
|
||||
nb_demarches_test: 0,
|
||||
nb_demarches_prod: 0,
|
||||
nb_demarches_prod_20: 0,
|
||||
nb_dossiers: 0,
|
||||
nb_dossiers_max: 0,
|
||||
nb_dossiers_traite: 0,
|
||||
nb_dossiers_dossier_en_instruction: 0
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a procedure en test' do
|
||||
let(:procedure) { create(:procedure, dossiers: dossiers) }
|
||||
let(:dossiers) do
|
||||
(1..7).flat_map do
|
||||
[
|
||||
create(:dossier, :en_construction),
|
||||
create(:dossier, :en_instruction),
|
||||
create(:dossier, :accepte)
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
it do
|
||||
is_expected.to include(
|
||||
ds_nb_demarches_actives: 0,
|
||||
ds_nb_demarches_archives: 0,
|
||||
ds_nb_demarches_brouillons: 1,
|
||||
nb_demarches_test: 1,
|
||||
nb_demarches_prod: 0,
|
||||
nb_demarches_prod_20: 0,
|
||||
nb_dossiers: 0,
|
||||
nb_dossiers_max: 0,
|
||||
nb_dossiers_traite: 0,
|
||||
nb_dossiers_dossier_en_instruction: 0
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a procedure en prod' do
|
||||
let(:procedure) { create(:procedure, aasm_state: 'publiee', dossiers: dossiers) }
|
||||
let(:dossiers) do
|
||||
[
|
||||
create(:dossier, :en_construction),
|
||||
create(:dossier, :en_instruction),
|
||||
create(:dossier, :accepte)
|
||||
]
|
||||
end
|
||||
|
||||
it do
|
||||
is_expected.to include(
|
||||
ds_nb_demarches_actives: 1,
|
||||
ds_nb_demarches_archives: 0,
|
||||
ds_nb_demarches_brouillons: 0,
|
||||
nb_demarches_test: 0,
|
||||
nb_demarches_prod: 1,
|
||||
nb_demarches_prod_20: 0,
|
||||
nb_dossiers: 3,
|
||||
nb_dossiers_max: 3,
|
||||
nb_dossiers_traite: 1,
|
||||
nb_dossiers_dossier_en_instruction: 1
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a procedure en prod and more than 20 dossiers' do
|
||||
let(:procedure) { create(:procedure, aasm_state: 'publiee', dossiers: dossiers) }
|
||||
let(:dossiers) do
|
||||
(1..7).flat_map do
|
||||
[
|
||||
create(:dossier, :en_construction),
|
||||
create(:dossier, :en_instruction),
|
||||
create(:dossier, :accepte)
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
it do
|
||||
is_expected.to include(
|
||||
ds_nb_demarches_actives: 1,
|
||||
ds_nb_demarches_archives: 0,
|
||||
ds_nb_demarches_brouillons: 0,
|
||||
nb_demarches_test: 0,
|
||||
nb_demarches_prod: 1,
|
||||
nb_demarches_prod_20: 1,
|
||||
nb_dossiers: 21,
|
||||
nb_dossiers_max: 21,
|
||||
nb_dossiers_traite: 7,
|
||||
nb_dossiers_dossier_en_instruction: 7
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue