Merge pull request #9873 from demarches-simplifiees/files_recovery

ETQ usager, agent de la fonction publique territoriale, je peux récupérer les dossiers d'un collègue absent
This commit is contained in:
LeSim 2024-03-15 13:53:32 +00:00 committed by GitHub
commit ca413a1035
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 1406 additions and 12 deletions

View file

@ -40,10 +40,10 @@
- elsif @state == 'choix'
= form_for :choice,
method: :patch,
data: { controller: 'radio-enabled-submit' },
data: { controller: 'enable-submit-if-checked' },
url: wizard_admin_procedure_groupe_instructeurs_path(@procedure) do |f|
%div{ data: { 'action': "click->radio-enabled-submit#click" } }
%div{ data: { 'action': "click->enable-submit-if-checked#click" } }
= render Dsfr::RadioButtonListComponent.new(form: f,
target: :state,
buttons: [ { label: 'À partir dun champ', value: 'routage_simple', hint: 'crée les groupes en fonction dun champ du formulaire' } ,
@ -54,4 +54,4 @@
%li
= link_to 'Retour', options_admin_procedure_groupe_instructeurs_path(@procedure), class: 'fr-btn fr-btn--secondary'
%li
%button.fr-btn{ disabled: true, data: { 'radio-enabled-submit-target': 'submit' } } Continuer
%button.fr-btn{ disabled: true, data: { 'enable-submit-if-checked-target': 'submit' } } Continuer

View file

@ -35,6 +35,9 @@ class AgentConnect::AgentController < ApplicationController
instructeur.update(agent_connect_id: user_info['sub'])
end
aci = AgentConnectInformation.find_or_initialize_by(instructeur:)
aci.update(user_info.slice('given_name', 'usual_name', 'email', 'sub', 'siret', 'organizational_unit', 'belonging_population', 'phone'))
sign_in(:user, instructeur.user)
redirect_to instructeur_procedures_path

View file

@ -0,0 +1,83 @@
class RecoveriesController < ApplicationController
before_action :ensure_agent_connect_is_used, except: [:nature, :post_nature, :support]
before_action :ensure_collectivite_territoriale, except: [:nature, :post_nature, :support]
def nature
end
def post_nature
if nature_params == 'collectivite'
redirect_to identification_recovery_path
else
redirect_to support_recovery_path(error: :other_nature)
end
end
def identification
@structure_name = structure_name
end
def post_identification
# cipher previous_user email
# to avoid leaks in the url
ciphered_email = cipher(previous_email)
redirect_to selection_recovery_path(ciphered_email:)
end
def selection
@previous_email = uncipher(params[:ciphered_email])
previous_user = User.find_by(email: @previous_email)
@recoverables = RecoveryService
.recoverable_procedures(previous_user:, siret:)
redirect_to support_recovery_path(error: :no_dossier) if @recoverables.empty?
end
def post_selection
previous_user = User.find_by(email: previous_email)
RecoveryService.recover_procedure!(previous_user:,
next_user: current_user,
siret:,
procedure_ids:)
redirect_to terminee_recovery_path
end
def terminee
end
def support
end
private
def nature_params = params[:nature]
def siret = current_instructeur.agent_connect_information.siret
def previous_email = params[:previous_email]
def procedure_ids = params[:procedure_ids].map(&:to_i)
def cipher(email) = message_verifier.generate(email, purpose: :agent_files_recovery, expires_in: 1.hour)
def uncipher(email) = message_verifier.verified(email, purpose: :agent_files_recovery) rescue nil
def structure_name
# we know that the structure exists because
# of the ensure_collectivite_territoriale guard
APIRechercheEntreprisesService.new.(siret:).value![:nom_complet]
end
def ensure_agent_connect_is_used
if current_instructeur&.agent_connect_information.nil?
redirect_to support_recovery_path(error: :must_use_agent_connect)
end
end
def ensure_collectivite_territoriale
if !APIRechercheEntreprisesService.collectivite_territoriale?(siret:)
redirect_to support_recovery_path(error: 'not_collectivite_territoriale')
end
end
end

View file

@ -0,0 +1,16 @@
module RecoverySelectionHelper
def recoverable_id_and_libelles(recoverables)
recoverables
.map { |r| [r[:procedure_id], nice_libelle(r)] }
end
private
def nice_libelle(recoverable)
sanitize(
" #{number_with_html_delimiter(recoverable[:procedure_id])}" \
" - #{recoverable[:libelle]} " \
"#{tag.span(pluralize(recoverable[:count], 'dossier'), class: 'fr-tag fr-tag--sm')}"
)
end
end

View file

@ -1,14 +1,17 @@
import { Controller } from '@hotwired/stimulus';
export class RadioEnabledSubmitController extends Controller {
export class EnableSubmitIfCheckedController extends Controller {
static targets = ['submit'];
declare readonly submitTarget: HTMLButtonElement;
click() {
if (
this.element.querySelectorAll('input[type="radio"]:checked').length > 0
this.element.querySelectorAll('input[type="radio"]:checked').length > 0 ||
this.element.querySelectorAll('input[type="checkbox"]:checked').length > 0
) {
this.submitTarget.disabled = false;
} else {
this.submitTarget.disabled = true;
}
}
}

View file

@ -0,0 +1,3 @@
class AgentConnectInformation < ApplicationRecord
belongs_to :instructeur
end

View file

@ -2,6 +2,8 @@ class Instructeur < ApplicationRecord
include UserFindByConcern
has_and_belongs_to_many :administrateurs
has_one :agent_connect_information, dependent: :destroy
has_many :assign_to, dependent: :destroy
has_many :groupe_instructeurs, -> { order(:label) }, through: :assign_to
has_many :unordered_groupe_instructeurs, through: :assign_to, source: :groupe_instructeur

View file

@ -12,9 +12,9 @@ class AgentConnectService
nonce = SecureRandom.hex(16)
uri = client.authorization_uri(
scope: [:openid, :email],
state: state,
nonce: nonce,
scope: [:openid, :email, :given_name, :usual_name, :organizational_unit, :belonging_population, :siret],
state:,
nonce:,
acr_values: 'eidas1'
)

View file

@ -0,0 +1,35 @@
class APIRechercheEntreprisesService
include Dry::Monads[:result]
def self.collectivite_territoriale?(siret:)
response = APIRechercheEntreprisesService.new.call(siret:)
return false if response.failure?
response.success&.dig(:complements, :collectivite_territoriale).present?
end
def call(siret:)
result = API::Client.new.(url: "#{url}?q=#{siret}")
return result if result.failure?
body = result.success.body
return Success(nil) if body[:results].empty?
# the api returns the matching structure in the first element if it exists
structure = body[:results][0]
# safety check : the api does fuzzy matching, so we need to check that the siret matches
return Failure() if structure[:matching_etablissements].all? { _1[:siret] != siret }
Success(structure)
end
private
def url
"#{API_RECHERCHE_ENTREPRISE_URL}/search"
end
end

View file

@ -0,0 +1,37 @@
class RecoveryService
def self.recoverable_procedures(previous_user:, siret:)
return [] if previous_user.nil?
previous_user.dossiers
.includes(:procedure)
.joins(:etablissement)
.where(etablissements: { siret: })
.pluck('procedures.id, procedures.libelle')
.tally
.map { |(procedure_id, libelle), count| { procedure_id:, libelle:, count: } }
.sort_by { |h| [-h[:count], h[:libelle]] }
end
def self.recover_procedure!(previous_user:, next_user:, siret:, procedure_ids:)
recoverable_procedure_ids = recoverable_procedures(previous_user: previous_user, siret: siret)
.map { _1[:procedure_id] }
dossiers = procedure_ids
.filter { |id| id.in?(recoverable_procedure_ids) }
.then do |p_ids|
previous_user.dossiers.joins(:procedure)
.where(procedure: { id: p_ids })
end
dossiers.pluck(:id).map do |id|
{
dossier_id: id,
from: previous_user.email,
from_support: false,
to: next_user.email
}
end.then { |array| DossierTransferLog.create(array) }
dossiers.update_all(user_id: next_user.id)
end
end

View file

@ -9,10 +9,10 @@
%h1 Routage à partir dun champ
= form_for :create_simple_routing,
method: :post,
data: { controller: 'radio-enabled-submit' },
data: { controller: 'enable-submit-if-checked' },
url: create_simple_routing_admin_procedure_groupe_instructeurs_path(@procedure) do |f|
%div{ data: { 'action': "click->radio-enabled-submit#click" } }
%div{ data: { 'action': "click->enable-submit-if-checked#click" } }
.notice
Sélectionner le champ à partir duquel créer des groupes dinstructeurs
- buttons_content = @procedure.active_revision.routable_types_de_champ.map { |tdc| { label: tdc.libelle, value: tdc.stable_id } }
@ -24,4 +24,4 @@
%li
= link_to 'Retour', options_admin_procedure_groupe_instructeurs_path(@procedure, state: :choix), class: 'fr-btn fr-btn--secondary'
%li
%button.fr-btn{ disabled: true, data: { disable_with: 'Création des groupes…', 'radio-enabled-submit-target': 'submit' } } Créer les groupes
%button.fr-btn{ disabled: true, data: { disable_with: 'Création des groupes…', 'enable-submit-if-checked-target': 'submit' } } Créer les groupes

View file

@ -0,0 +1,21 @@
- content_for(:title) { "Identification du propriétaire" }
.fr-container.fr-my-6w
%h1 Récupération de dossiers
%h2 Identification du propriétaire des dossiers
%p Votre organisation est « #{@structure_name} » identifiée par le SIRET #{current_instructeur.agent_connect_information.siret}
= form_with do |f|
.fr-input-group
%label.fr-label{ for: "email" }
Email du propriétaire des dossiers
%span.fr-hint-text= t('email', scope: [:activerecord, :attributes, :default_attributes, :hints])
= f.email_field :previous_email,
required: true,
autocomplete: 'off',
class: 'fr-input width-66',
id: 'email'
%button.fr-btn Continuer

View file

@ -0,0 +1,18 @@
- content_for(:title) { "Nature des dossiers" }
.fr-container.fr-my-6w
%h1.fr-h1 Récupération de dossiers
%h2.fr-h2 Nature des dossiers
= form_with data: { controller: 'enable-submit-if-checked' } do |f|
- buttons = [{ label: 'des dossiers concernant une collectivité territoriale',
value: 'collectivite',
hint: '(DETR, autres demandes de subvention, consultation du domaine, ...)' },
{ label: 'autre', value: 'autre'}]
%div{ data: { 'action': "click->enable-submit-if-checked#click" } }
= render Dsfr::RadioButtonListComponent.new(form: f, target: :nature, buttons: buttons) do
%legend#radio-hint-element-legend.fr-fieldset__legend--regular.fr-fieldset__legend Quel type de dossier souhaitez vous récupérer ?
%button.fr-btn{ disabled: true, data: { 'enable-submit-if-checked-target': 'submit' } } Continuer

View file

@ -0,0 +1,20 @@
- content_for(:title) { "Sélection des démarches" }
.fr-container.fr-my-6w
%h1.fr-h1 Récupération de dossiers
%h2.fr-h2 Sélection des démarches
= form_tag nil, data: { controller: 'enable-submit-if-checked' } do
%fieldset#checkboxes.fr-fieldset{ 'aria-labelledby': "checkboxes-legend checkboxes-messages",
data: { 'action': "click->enable-submit-if-checked#click" } }
%legend#checkboxes-legend.fr-fieldset__legend--regular.fr-fieldset__legend
Sélectionner les démarches que vous souhaitez récuperer.
- recoverable_id_and_libelles(@recoverables).each do |procedure_id, libelle|
.fr-fieldset__element
.fr-checkbox-group
= check_box_tag 'procedure_ids[]', procedure_id, false, class: 'fr-checkbox', id: procedure_id
= label_tag procedure_id, libelle, class: 'fr-label'
= hidden_field_tag 'previous_email', @previous_email
%button.fr-btn{ disabled: true, data: { 'enable-submit-if-checked-target': 'submit' } } Continuer

View file

@ -0,0 +1,36 @@
- content_for(:title) { "Contactez le support" }
.fr-container.fr-my-6w
%h1.fr-h1 Récupération de dossiers
- case params[:error]
- when 'other_nature', 'not_collectivite_territoriale'
%p Votre situation nécessite un traitement particulier.
= mail_to(CONTACT_EMAIL,
'Contactez le support',
subject: 'Récupération de dossiers',
class: 'fr-btn')
- when 'must_use_agent_connect'
%p Vous devez utiliser le portail AgentConnect pour récupérer vos dossiers.
= link_to(agent_connect_login_path, class: "fr-btn fr-connect") do
%span.fr-connect__login
= t('signin_with', scope: [:agent_connect, :agent, :index])
%span.fr-connect__brand AgentConnect
%p Vous n'avez pas encore de compte AgentConnect ? Vous pouvez en créer un en utilisant MonComptePro.
= link_to 'MonComptePro', 'https://moncomptepro.beta.gouv.fr', class: 'fr-btn'
- when 'no_dossier'
%p Lʼadresse email « #{cookies[:retrieve_email]} » que vous avez renseignée nʼa pas de dossier concernant votre organisation.
%ul.fr-btns-group.fr-btns-group--inline
%li
= link_to 'Essayer avec une autre adresse email', identification_recovery_path, class: 'fr-btn'
%li
= mail_to(CONTACT_EMAIL,
'Contactez le support',
subject: 'Récupération de dossiers',
class: 'fr-btn fr-btn--secondary')

View file

@ -0,0 +1,10 @@
- content_for(:title) { "Récupération terminée" }
.fr-container.fr-my-6w
%h1.fr-h1 Récupération de dossiers
%h2.fr-h2 Récupération terminée
%p Les dossiers vous ont été réaffectés.
= link_to "Voir mes dossiers", dossiers_path, class: "fr-btn"

View file

@ -8,6 +8,7 @@ API_PARTICULIER_URL = ENV.fetch("API_PARTICULIER_URL", "https://particulier.api.
API_TCHAP_URL = ENV.fetch("API_TCHAP_URL", "https://matrix.agent.tchap.gouv.fr/_matrix/identity/api/v1")
API_COJO_URL = ENV.fetch("API_COJO_URL", nil)
API_RNF_URL = ENV.fetch("API_RNF_URL", "https://rnf.dso.numerique-interieur.com")
API_RECHERCHE_ENTREPRISE_URL = ENV.fetch("API_RECHERCHE_ENTREPRISE_URL", "https://recherche-entreprises.api.gouv.fr")
HELPSCOUT_API_URL = ENV.fetch("HELPSCOUT_API_URL", "https://api.helpscout.net/v2")
SENDINBLUE_API_URL = ENV.fetch("SENDINBLUE_API_URL", "https://in-automate.sendinblue.com/api/v2")
SENDINBLUE_API_V3_URL = ENV.fetch("SENDINBLUE_API_V3_URL", "https://api.sendinblue.com/v3")

View file

@ -699,6 +699,20 @@ Rails.application.routes.draw do
end
end
resource :recovery, only: [], path: :recuperation_de_dossiers do
collection do
get :nature
post :nature, action: :post_nature
get :identification
post :identification, action: :post_identification
get :selection
post :selection, action: :post_selection
get :terminee
get :support
end
root action: :nature
end
#
# Legacy routes
#

View file

@ -0,0 +1,17 @@
class CreateAgentConnectInformations < ActiveRecord::Migration[7.0]
def change
create_table :agent_connect_informations do |t|
t.references :instructeur, null: false, foreign_key: true
t.string :given_name, null: false
t.string :usual_name, null: false
t.string :email, null: false
t.string :sub, null: false
t.string :siret
t.string :organizational_unit
t.string :belonging_population
t.string :phone
t.timestamps
end
end
end

View file

@ -91,6 +91,21 @@ ActiveRecord::Schema[7.0].define(version: 2024_02_27_163855) do
t.index ["procedure_id"], name: "index_administrateurs_procedures_on_procedure_id"
end
create_table "agent_connect_informations", force: :cascade do |t|
t.string "belonging_population"
t.datetime "created_at", null: false
t.string "email", null: false
t.string "given_name", null: false
t.bigint "instructeur_id", null: false
t.string "organizational_unit"
t.string "phone"
t.string "siret"
t.string "sub", null: false
t.datetime "updated_at", null: false
t.string "usual_name", null: false
t.index ["instructeur_id"], name: "index_agent_connect_informations_on_instructeur_id"
end
create_table "api_tokens", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.bigint "administrateur_id", null: false
t.bigint "allowed_procedure_ids", array: true
@ -1179,6 +1194,7 @@ ActiveRecord::Schema[7.0].define(version: 2024_02_27_163855) do
add_foreign_key "administrateurs_instructeurs", "instructeurs"
add_foreign_key "administrateurs_procedures", "administrateurs"
add_foreign_key "administrateurs_procedures", "procedures"
add_foreign_key "agent_connect_informations", "instructeurs"
add_foreign_key "api_tokens", "administrateurs"
add_foreign_key "archives_groupe_instructeurs", "archives"
add_foreign_key "archives_groupe_instructeurs", "groupe_instructeurs"

View file

@ -30,7 +30,7 @@ describe AgentConnect::AgentController, type: :controller do
context 'when the callback code is correct' do
let(:code) { 'correct' }
let(:state) { original_state }
let(:user_info) { { 'sub' => 'sub', 'email' => ' I@email.com' } }
let(:user_info) { { 'sub' => 'sub', 'email' => ' I@email.com', 'given_name' => 'given', 'usual_name' => 'usual' } }
context 'and user_info returns some info' do
before do

View file

@ -0,0 +1,139 @@
describe RecoveriesController, type: :controller do
include Dry::Monads[:result]
describe 'GET #nature' do
subject { get :nature }
it { is_expected.to have_http_status(:success) }
end
describe 'POST #post_nature' do
subject { post :post_nature, params: { nature: nature } }
context 'when nature is collectivite' do
let(:nature) { 'collectivite' }
it { is_expected.to redirect_to(identification_recovery_path) }
end
context 'when nature is not collectivite' do
let(:nature) { 'other' }
it { is_expected.to redirect_to(support_recovery_path(error: :other_nature)) }
end
end
describe 'Get #support' do
subject { get :support }
it { is_expected.to have_http_status(:success) }
end
describe 'ensure_agent_connect_is_used' do
subject { post :selection }
before do
allow(controller).to receive(:ensure_collectivite_territoriale).and_return(true)
allow(controller).to receive(:selection).and_return(true)
end
context 'when agent connect is used' do
let(:instructeur) { create(:instructeur, :with_agent_connect_information) }
before do
allow(controller).to receive(:current_instructeur).and_return(instructeur)
end
it { is_expected.to have_http_status(:success) }
end
context 'when agent connect is not used' do
it { is_expected.to redirect_to(support_recovery_path(error: :must_use_agent_connect)) }
end
end
describe 'ensure_collectivite_territoriale' do
subject { post :selection }
before do
allow(controller).to receive(:ensure_agent_connect_is_used).and_return(true)
allow(controller).to receive(:siret).and_return('123')
allow(controller).to receive(:selection).and_return(true)
end
context 'when collectivite territoriale' do
before do
allow(APIRechercheEntreprisesService).to receive(:collectivite_territoriale?).and_return(true)
end
it { is_expected.to have_http_status(:success) }
end
context 'when not collectivite territoriale' do
before do
allow(APIRechercheEntreprisesService).to receive(:collectivite_territoriale?).and_return(false)
end
it { is_expected.to redirect_to(support_recovery_path(error: 'not_collectivite_territoriale')) }
end
end
context 'when the current instructeur used agent connect and works for a collectivite territoriale' do
let(:instructeur) { create(:instructeur, :with_agent_connect_information) }
let(:api_recherche_result) do
{ nom_complet: 'name', complements: { collectivite_territoriale: { is: :present } } }
end
before do
allow(controller).to receive(:current_instructeur).and_return(instructeur)
allow_any_instance_of(APIRechercheEntreprisesService).to receive(:call)
.and_return(Success(api_recherche_result))
end
describe 'GET #identification' do
subject { get :identification }
it { is_expected.to have_http_status(:success) }
end
describe 'POST #post_identification' do
subject { post :post_identification, params: { previous_email: 'email@a.com' } }
it do
response = subject
expect(response).to have_http_status(:redirect)
expect(response.location).to start_with(selection_recovery_url)
end
end
describe 'GET #selection' do
subject { get :selection }
context 'when there are no recoverable procedures' do
before do
allow(RecoveryService).to receive(:recoverable_procedures).and_return([])
end
it { is_expected.to redirect_to(support_recovery_path(error: :no_dossier)) }
end
context 'when there are recoverable procedures' do
let(:recoverable_procedures) { [[1, 'libelle', 2]] }
before do
allow(RecoveryService).to receive(:recoverable_procedures).and_return(recoverable_procedures)
end
it { is_expected.to have_http_status(:success) }
end
end
describe 'POST #post_selection' do
subject { post :post_selection, params: { procedure_ids: [1] } }
before { expect(RecoveryService).to receive(:recover_procedure!) }
it { is_expected.to redirect_to(terminee_recovery_path) }
end
end
end

View file

@ -0,0 +1,12 @@
FactoryBot.define do
factory :agent_connect_information do
email { 'i@agent_connect.fr' }
given_name { 'John' }
usual_name { 'Doe' }
sub { '123456789' }
siret { '12345678901234' }
organizational_unit { 'Ministère A.M.E.R.' }
belonging_population { 'stagiaire' }
phone { '0123456789' }
end
end

View file

@ -10,5 +10,11 @@ FactoryBot.define do
email { generate(:instructeur_email) }
password { 'somethingverycomplated!' }
end
trait :with_agent_connect_information do
after(:create) do |instructeur, _evaluator|
create(:agent_connect_information, instructeur: instructeur)
end
end
end
end

View file

@ -0,0 +1,640 @@
{
"results": [
{
"siren": "200065415",
"nom_complet": "COMMUNE DE LA HAGUE",
"nom_raison_sociale": "COMMUNE DE LA HAGUE",
"sigle": null,
"nombre_etablissements": 51,
"nombre_etablissements_ouverts": 48,
"siege": {
"activite_principale": "84.11Z",
"activite_principale_registre_metier": null,
"annee_tranche_effectif_salarie": "2021",
"adresse": "8 RUE DES TOHAGUES 50440 LA HAGUE",
"caractere_employeur": null,
"cedex": null,
"code_pays_etranger": null,
"code_postal": "50440",
"commune": "50041",
"complement_adresse": null,
"coordonnees": "49.66215,-1.830805",
"date_creation": "2017-01-01",
"date_debut_activite": "2017-01-01",
"date_mise_a_jour": "2023-11-27T13:18:35",
"departement": "50",
"distribution_speciale": null,
"est_siege": true,
"etat_administratif": "A",
"geo_adresse": "8 Rue des Tohagues 50440 La Hague",
"geo_id": "50041_0120_00008",
"indice_repetition": null,
"latitude": "49.66215",
"libelle_cedex": null,
"libelle_commune": "LA HAGUE",
"libelle_commune_etranger": null,
"libelle_pays_etranger": null,
"libelle_voie": "DES TOHAGUES",
"liste_enseignes": [
"MAIRIE"
],
"liste_finess": null,
"liste_id_bio": null,
"liste_idcc": [
"5021"
],
"liste_id_organisme_formation": null,
"liste_rge": null,
"liste_uai": null,
"longitude": "-1.830805",
"nom_commercial": null,
"numero_voie": "8",
"region": "28",
"siret": "20006541500016",
"tranche_effectif_salarie": "32",
"type_voie": "RUE"
},
"activite_principale": "84.11Z",
"categorie_entreprise": "ETI",
"caractere_employeur": null,
"annee_categorie_entreprise": "2021",
"date_creation": "2017-01-01",
"date_mise_a_jour": "2023-11-30T10:17:45",
"dirigeants": [],
"etat_administratif": "A",
"nature_juridique": "7210",
"section_activite_principale": "O",
"tranche_effectif_salarie": "32",
"annee_tranche_effectif_salarie": "2021",
"statut_diffusion": "O",
"matching_etablissements": [
{
"activite_principale": "84.11Z",
"annee_tranche_effectif_salarie": "2021",
"adresse": "8 RUE DES TOHAGUES 50440 LA HAGUE",
"caractere_employeur": null,
"code_postal": "50440",
"commune": "50041",
"date_creation": "2017-01-01",
"date_debut_activite": "2017-01-01",
"est_siege": true,
"etat_administratif": "A",
"geo_id": "50041_0120_00008",
"latitude": "49.66215",
"libelle_commune": "LA HAGUE",
"liste_enseignes": [
"MAIRIE"
],
"liste_finess": null,
"liste_id_bio": null,
"liste_idcc": [
"5021"
],
"liste_id_organisme_formation": null,
"liste_rge": null,
"liste_uai": null,
"longitude": "-1.830805",
"nom_commercial": null,
"region": "28",
"siret": "20006541500016",
"tranche_effectif_salarie": "32"
}
],
"finances": {},
"complements": {
"collectivite_territoriale": {
"code": "50041",
"code_insee": "50041",
"elus": [
{
"nom": "ADOUE",
"prenoms": "Chantal",
"annee_de_naissance": "19/0",
"fonction": null,
"sexe": "F"
},
{
"nom": "ALLENO",
"prenoms": "Gwladys",
"annee_de_naissance": "04/0",
"fonction": "8ème adjoint au Maire",
"sexe": "F"
},
{
"nom": "BEAUMONT",
"prenoms": "Monique",
"annee_de_naissance": "29/0",
"fonction": null,
"sexe": "F"
},
{
"nom": "BECQUET",
"prenoms": "Dominique",
"annee_de_naissance": "17/0",
"fonction": null,
"sexe": "M"
},
{
"nom": "BEDEL",
"prenoms": "Pauline",
"annee_de_naissance": "29/0",
"fonction": "Maire délégué",
"sexe": "F"
},
{
"nom": "BELHOMME",
"prenoms": "Dominique",
"annee_de_naissance": "12/0",
"fonction": null,
"sexe": "M"
},
{
"nom": "BELHOMME",
"prenoms": "Jérôme",
"annee_de_naissance": "14/0",
"fonction": "1er adjoint au Maire",
"sexe": "M"
},
{
"nom": "BELHOMME",
"prenoms": "Jérôme",
"annee_de_naissance": "14/0",
"fonction": "Maire délégué",
"sexe": "M"
},
{
"nom": "BONNISSENT",
"prenoms": "Marie-Suzanne",
"annee_de_naissance": "05/0",
"fonction": null,
"sexe": "F"
},
{
"nom": "CANOVILLE",
"prenoms": "Laurent",
"annee_de_naissance": "29/0",
"fonction": null,
"sexe": "M"
},
{
"nom": "CERVANTÈS",
"prenoms": "Simon",
"annee_de_naissance": "06/0",
"fonction": "Maire délégué",
"sexe": "M"
},
{
"nom": "CHARDOT",
"prenoms": "Mélanie",
"annee_de_naissance": "12/0",
"fonction": null,
"sexe": "F"
},
{
"nom": "CHARLES",
"prenoms": "Véronique",
"annee_de_naissance": "04/0",
"fonction": null,
"sexe": "F"
},
{
"nom": "COLLET",
"prenoms": "Christian",
"annee_de_naissance": "20/1",
"fonction": null,
"sexe": "M"
},
{
"nom": "CRANOIS",
"prenoms": "Louis",
"annee_de_naissance": "16/0",
"fonction": "Maire délégué",
"sexe": "M"
},
{
"nom": "DALMONT",
"prenoms": "Hubert",
"annee_de_naissance": "29/0",
"fonction": null,
"sexe": "M"
},
{
"nom": "DAMOURETTE",
"prenoms": "Etienne",
"annee_de_naissance": "02/1",
"fonction": null,
"sexe": "M"
},
{
"nom": "DELACOUR",
"prenoms": "Thérèse",
"annee_de_naissance": "31/0",
"fonction": "Maire délégué",
"sexe": "F"
},
{
"nom": "DESBOIS",
"prenoms": "Noëmie",
"annee_de_naissance": "14/0",
"fonction": null,
"sexe": "F"
},
{
"nom": "DIGARD",
"prenoms": "Antoine",
"annee_de_naissance": "03/0",
"fonction": "3ème adjoint au Maire",
"sexe": "M"
},
{
"nom": "DIGARD",
"prenoms": "Antoine",
"annee_de_naissance": "03/0",
"fonction": "Maire délégué",
"sexe": "M"
},
{
"nom": "DUBOST",
"prenoms": "Hubert",
"annee_de_naissance": "17/1",
"fonction": "Maire délégué",
"sexe": "M"
},
{
"nom": "DUBOST",
"prenoms": "Nathalie",
"annee_de_naissance": "07/0",
"fonction": "4ème adjoint au Maire",
"sexe": "F"
},
{
"nom": "FLEURY",
"prenoms": "Jean-Marie",
"annee_de_naissance": "21/0",
"fonction": "11ème adjoint au Maire",
"sexe": "M"
},
{
"nom": "FRACHET",
"prenoms": "Nadine",
"annee_de_naissance": "26/0",
"fonction": null,
"sexe": "F"
},
{
"nom": "FRIGOUT",
"prenoms": "Jean-Marc",
"annee_de_naissance": "20/1",
"fonction": null,
"sexe": "M"
},
{
"nom": "GASNIER",
"prenoms": "Philippe",
"annee_de_naissance": "22/1",
"fonction": "5ème adjoint au Maire",
"sexe": "M"
},
{
"nom": "GASNIER",
"prenoms": "Philippe",
"annee_de_naissance": "22/1",
"fonction": "Maire délégué",
"sexe": "M"
},
{
"nom": "GAUMAIN",
"prenoms": "Mathieu",
"annee_de_naissance": "06/0",
"fonction": null,
"sexe": "M"
},
{
"nom": "GOACHET",
"prenoms": "Joseph",
"annee_de_naissance": "27/0",
"fonction": null,
"sexe": "M"
},
{
"nom": "GROF",
"prenoms": "Béatrice",
"annee_de_naissance": "17/0",
"fonction": "2ème adjoint au Maire",
"sexe": "F"
},
{
"nom": "GUILLEMETTE",
"prenoms": "Nathalie",
"annee_de_naissance": "30/0",
"fonction": "6ème adjoint au Maire",
"sexe": "F"
},
{
"nom": "HAMELIN",
"prenoms": "Magali",
"annee_de_naissance": "16/0",
"fonction": null,
"sexe": "F"
},
{
"nom": "HENRY",
"prenoms": "Claude",
"annee_de_naissance": "30/0",
"fonction": null,
"sexe": "M"
},
{
"nom": "JACQUET-ROCQUET",
"prenoms": "Sophie",
"annee_de_naissance": "26/0",
"fonction": null,
"sexe": "F"
},
{
"nom": "JOURDAIN",
"prenoms": "Patrick",
"annee_de_naissance": "28/0",
"fonction": "7ème adjoint au Maire",
"sexe": "M"
},
{
"nom": "JOURDAIN",
"prenoms": "Patrick",
"annee_de_naissance": "28/0",
"fonction": "Maire délégué",
"sexe": "M"
},
{
"nom": "JUMELIN",
"prenoms": "Pascale",
"annee_de_naissance": "24/0",
"fonction": "Maire délégué",
"sexe": "F"
},
{
"nom": "LADVENU",
"prenoms": "Nathalie",
"annee_de_naissance": "14/0",
"fonction": null,
"sexe": "F"
},
{
"nom": "LAGALLE",
"prenoms": "Marie-Laure",
"annee_de_naissance": "23/0",
"fonction": null,
"sexe": "F"
},
{
"nom": "LAPPREND",
"prenoms": "Marie",
"annee_de_naissance": "08/1",
"fonction": "10ème adjoint au Maire",
"sexe": "F"
},
{
"nom": "LARGERIE",
"prenoms": "Anne",
"annee_de_naissance": "03/0",
"fonction": null,
"sexe": "F"
},
{
"nom": "LAVENU",
"prenoms": "Patrick",
"annee_de_naissance": "01/0",
"fonction": null,
"sexe": "M"
},
{
"nom": "LECOSTEY",
"prenoms": "Fabrice",
"annee_de_naissance": "25/0",
"fonction": null,
"sexe": "M"
},
{
"nom": "LECOSTEY",
"prenoms": "Jean",
"annee_de_naissance": "14/1",
"fonction": null,
"sexe": "M"
},
{
"nom": "LEDAUPHIN",
"prenoms": "Nathalie",
"annee_de_naissance": "05/0",
"fonction": null,
"sexe": "F"
},
{
"nom": "LEFRÉTEUR",
"prenoms": "Emmanuel",
"annee_de_naissance": "10/0",
"fonction": null,
"sexe": "M"
},
{
"nom": "LEGELEUX",
"prenoms": "Yann",
"annee_de_naissance": "20/0",
"fonction": null,
"sexe": "M"
},
{
"nom": "LELONG",
"prenoms": "Nadine",
"annee_de_naissance": "29/0",
"fonction": null,
"sexe": "F"
},
{
"nom": "LELONG",
"prenoms": "Sébastien",
"annee_de_naissance": "15/0",
"fonction": "Maire délégué",
"sexe": "M"
},
{
"nom": "LERENDU",
"prenoms": "Patrick",
"annee_de_naissance": "08/1",
"fonction": "Maire délégué",
"sexe": "M"
},
{
"nom": "LESEIGNEUR-COURVAL",
"prenoms": "Thérèse",
"annee_de_naissance": "22/0",
"fonction": "Maire délégué",
"sexe": "F"
},
{
"nom": "LETOURNEUR",
"prenoms": "Bruno",
"annee_de_naissance": "21/0",
"fonction": "Maire délégué",
"sexe": "M"
},
{
"nom": "LUPO",
"prenoms": "Antoine",
"annee_de_naissance": "16/1",
"fonction": "Maire délégué",
"sexe": "M"
},
{
"nom": "MAHIER",
"prenoms": "Manuela",
"annee_de_naissance": "30/1",
"fonction": "Maire",
"sexe": "F"
},
{
"nom": "MAUGÉ",
"prenoms": "Caroline",
"annee_de_naissance": "13/0",
"fonction": null,
"sexe": "F"
},
{
"nom": "MERCIER",
"prenoms": "Philippe",
"annee_de_naissance": "06/0",
"fonction": "9ème adjoint au Maire",
"sexe": "M"
},
{
"nom": "MERCIER",
"prenoms": "Philippe",
"annee_de_naissance": "06/0",
"fonction": "Maire délégué",
"sexe": "M"
},
{
"nom": "MONHUREL",
"prenoms": "Pascal",
"annee_de_naissance": "23/0",
"fonction": "Maire délégué",
"sexe": "M"
},
{
"nom": "NICOLLE",
"prenoms": "Stéphanie",
"annee_de_naissance": "21/1",
"fonction": null,
"sexe": "F"
},
{
"nom": "NOEL",
"prenoms": "Nelly",
"annee_de_naissance": "24/0",
"fonction": null,
"sexe": "F"
},
{
"nom": "PELLERIN",
"prenoms": "Eric",
"annee_de_naissance": "17/0",
"fonction": "Maire délégué",
"sexe": "M"
},
{
"nom": "PERROTTE",
"prenoms": "Thomas",
"annee_de_naissance": "15/0",
"fonction": null,
"sexe": "M"
},
{
"nom": "RENOUF",
"prenoms": "Jean-Luc",
"annee_de_naissance": "17/0",
"fonction": "Maire délégué",
"sexe": "M"
},
{
"nom": "ROUCAN",
"prenoms": "Robert",
"annee_de_naissance": "27/0",
"fonction": null,
"sexe": "M"
},
{
"nom": "SANSON",
"prenoms": "Fabienne",
"annee_de_naissance": "10/0",
"fonction": null,
"sexe": "F"
},
{
"nom": "SANSON",
"prenoms": "Noël",
"annee_de_naissance": "25/1",
"fonction": null,
"sexe": "M"
},
{
"nom": "SEBIRE",
"prenoms": "Marine",
"annee_de_naissance": "07/0",
"fonction": null,
"sexe": "F"
},
{
"nom": "TARDIF",
"prenoms": "Pierre",
"annee_de_naissance": "13/0",
"fonction": null,
"sexe": "M"
},
{
"nom": "TESTELIN",
"prenoms": "Sébastien",
"annee_de_naissance": "03/0",
"fonction": null,
"sexe": "M"
},
{
"nom": "TOLLEMER",
"prenoms": "Pierre",
"annee_de_naissance": "05/0",
"fonction": null,
"sexe": "M"
},
{
"nom": "TRAVERT",
"prenoms": "Laurent",
"annee_de_naissance": "24/0",
"fonction": null,
"sexe": "M"
}
],
"niveau": "commune"
},
"convention_collective_renseignee": true,
"egapro_renseignee": false,
"est_association": false,
"est_bio": false,
"est_entrepreneur_individuel": false,
"est_entrepreneur_spectacle": true,
"est_ess": false,
"est_finess": false,
"est_organisme_formation": false,
"est_qualiopi": false,
"liste_id_organisme_formation": null,
"est_rge": false,
"est_service_public": true,
"est_societe_mission": false,
"est_uai": true,
"identifiant_association": null,
"statut_entrepreneur_spectacle": "valide"
}
}
],
"total_results": 1,
"page": 1,
"per_page": 10,
"total_pages": 1
}

View file

@ -0,0 +1,122 @@
{
"results": [
{
"siren": "130025265",
"nom_complet": "DIRECTION INTERMINISTERIELLE DU NUMERIQUE (DINUM)",
"nom_raison_sociale": "DIRECTION INTERMINISTERIELLE DU NUMERIQUE",
"sigle": "DINUM",
"nombre_etablissements": 1,
"nombre_etablissements_ouverts": 1,
"siege": {
"activite_principale": "84.11Z",
"activite_principale_registre_metier": null,
"annee_tranche_effectif_salarie": "2021",
"adresse": "20 AV DE SEGUR 75007 PARIS 7",
"caractere_employeur": null,
"cedex": null,
"code_pays_etranger": null,
"code_postal": "75007",
"commune": "75107",
"complement_adresse": null,
"coordonnees": "48.850699,2.308628",
"date_creation": "2017-05-24",
"date_debut_activite": "2017-05-24",
"date_mise_a_jour": "2023-11-27T13:16:04",
"departement": "75",
"distribution_speciale": null,
"est_siege": true,
"etat_administratif": "A",
"geo_adresse": "20 Avenue de Ségur 75007 Paris",
"geo_id": "75107_8909_00020",
"indice_repetition": null,
"latitude": "48.850699",
"libelle_cedex": null,
"libelle_commune": "PARIS 7",
"libelle_commune_etranger": null,
"libelle_pays_etranger": null,
"libelle_voie": "DE SEGUR",
"liste_enseignes": null,
"liste_finess": null,
"liste_id_bio": null,
"liste_idcc": null,
"liste_id_organisme_formation": null,
"liste_rge": null,
"liste_uai": null,
"longitude": "2.308628",
"nom_commercial": null,
"numero_voie": "20",
"region": "11",
"siret": "13002526500013",
"tranche_effectif_salarie": "22",
"type_voie": "AV"
},
"activite_principale": "84.11Z",
"categorie_entreprise": "PME",
"caractere_employeur": null,
"annee_categorie_entreprise": "2021",
"date_creation": "2017-05-24",
"date_mise_a_jour": "2023-11-30T10:17:13",
"dirigeants": [],
"etat_administratif": "A",
"nature_juridique": "7120",
"section_activite_principale": "O",
"tranche_effectif_salarie": "22",
"annee_tranche_effectif_salarie": "2021",
"statut_diffusion": "O",
"matching_etablissements": [
{
"activite_principale": "84.11Z",
"annee_tranche_effectif_salarie": "2021",
"adresse": "20 AV DE SEGUR 75007 PARIS 7",
"caractere_employeur": null,
"code_postal": "75007",
"commune": "75107",
"date_creation": "2017-05-24",
"date_debut_activite": "2017-05-24",
"est_siege": true,
"etat_administratif": "A",
"geo_id": "75107_8909_00020",
"latitude": "48.850699",
"libelle_commune": "PARIS 7",
"liste_enseignes": null,
"liste_finess": null,
"liste_id_bio": null,
"liste_idcc": null,
"liste_id_organisme_formation": null,
"liste_rge": null,
"liste_uai": null,
"longitude": "2.308628",
"nom_commercial": null,
"region": "11",
"siret": "13002526500013",
"tranche_effectif_salarie": "22"
}
],
"finances": {},
"complements": {
"collectivite_territoriale": null,
"convention_collective_renseignee": false,
"egapro_renseignee": false,
"est_association": false,
"est_bio": false,
"est_entrepreneur_individuel": false,
"est_entrepreneur_spectacle": false,
"est_ess": false,
"est_finess": false,
"est_organisme_formation": false,
"est_qualiopi": false,
"liste_id_organisme_formation": null,
"est_rge": false,
"est_service_public": true,
"est_societe_mission": false,
"est_uai": false,
"identifiant_association": null,
"statut_entrepreneur_spectacle": null
}
}
],
"total_results": 1,
"page": 1,
"per_page": 10,
"total_pages": 1
}

View file

@ -0,0 +1,60 @@
describe 'APIRechercheEntreprisesService' do
include Dry::Monads[:result]
OK = Data.define(:body, :response)
def load_json(file_name)
Rails.root.join("spec/fixtures/files/api_recherche_entreprises/#{file_name}.json")
.then { File.read(_1) }
.then { JSON.parse(_1).with_indifferent_access }
end
let(:col_ter_json) { load_json('col_ter_20006541500016') }
let(:dinum_json) { load_json('dinum_13002526500013') }
describe '.collectivite_territoriale' do
let(:client_response) { Success(OK[json_response, '']) }
subject { APIRechercheEntreprisesService.collectivite_territoriale?(siret:) }
before { expect_any_instance_of(API::Client).to receive(:call).and_return(client_response) }
context 'when the api returns some results' do
let(:json_response) { col_ter_json }
context 'and the siret match' do
context 'and the structure is a collectivite territoriale' do
let(:siret) { '20006541500016' }
it { is_expected.to be true }
end
context 'and the structure is not a collectivite territoriale' do
let(:json_response) { dinum_json }
let(:siret) { '13002526500013' }
it { is_expected.to be false }
end
end
context 'and the siret does not match' do
let(:siret) { '20006541500666' }
it { is_expected.to be false }
end
end
context 'when the api returns no result' do
let(:json_response) { { results: [] } }
let(:siret) { '20006541500016' }
it { is_expected.to be false }
end
context 'when the api returns an error' do
let(:client_response) { Failure() }
let(:siret) { '20006541500016' }
it { is_expected.to be false }
end
end
end

View file

@ -0,0 +1,80 @@
RSpec.describe RecoveryService, type: :service do
describe '.recoverable_procedures' do
subject { described_class.recoverable_procedures(previous_user:, siret:) }
context 'when the previous_user is nil' do
let(:previous_user) { nil }
let(:siret) { '123' }
it 'returns []' do
expect(subject).to eq([])
end
end
context 'when the previous_user has some dossiers' do
let(:previous_user) { create(:user) }
let(:procedure_1) { create(:procedure) }
let(:siret) { '123' }
let(:procedure_2) { create(:procedure) }
let(:another_siret) { 'another_123' }
before do
3.times do
create(:dossier, procedure: procedure_1,
etablissement: create(:etablissement, siret:),
user: previous_user)
end
create(:dossier, procedure: procedure_2,
etablissement: create(:etablissement, siret: another_siret),
user: previous_user)
end
it 'returns the procedures with their count' do
expect(subject).to eq([{ procedure_id: procedure_1.id, libelle: procedure_1.libelle, count: 3 }])
end
end
end
describe '.recover_procedure!' do
subject { described_class.recover_procedure!(previous_user:, next_user:, siret:, procedure_ids:) }
context 'when the previous_user has some dossiers' do
let!(:previous_user) { create(:user) }
let!(:next_user) { create(:user) }
let!(:procedure_1) { create(:procedure) }
let!(:siret) { '123' }
let!(:procedure_2) { create(:procedure) }
let!(:another_siret) { 'another_123' }
let!(:dossiers_to_recover) do
3.times do
create(:dossier, procedure: procedure_1,
etablissement: create(:etablissement, siret:),
user: previous_user)
end
end
let!(:dossiers_not_to_recover) do
create(:dossier, procedure: procedure_2,
etablissement: create(:etablissement, siret: another_siret),
user: previous_user)
end
let(:procedure_ids) { [procedure_1.id] }
it 'moves the files to the next user' do
subject
expect(next_user.dossiers.count).to eq(3)
dossier_transfer_log = next_user.dossiers.first.transfer_logs.first
expect(dossier_transfer_log.from).to eq(previous_user.email)
expect(dossier_transfer_log.to).to eq(next_user.email)
end
end
end
end