commit
a14ea5f0f4
103 changed files with 1208 additions and 153 deletions
|
@ -490,7 +490,7 @@ GEM
|
|||
byebug (~> 11.0)
|
||||
pry (~> 0.13.0)
|
||||
public_suffix (4.0.6)
|
||||
puma (5.3.1)
|
||||
puma (5.5.1)
|
||||
nio4r (~> 2.0)
|
||||
pundit (2.1.0)
|
||||
activesupport (>= 3.0.0)
|
||||
|
|
30
app/assets/stylesheets/cnaf.scss
Normal file
30
app/assets/stylesheets/cnaf.scss
Normal file
|
@ -0,0 +1,30 @@
|
|||
@import "constants";
|
||||
@import "colors";
|
||||
|
||||
table.cnaf {
|
||||
margin: 2 * $default-padding 0 $default-padding $default-padding;
|
||||
width: 100%;
|
||||
|
||||
caption {
|
||||
font-weight: bold;
|
||||
margin-left: - $default-padding;
|
||||
margin-bottom: $default-spacer;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
font-weight: normal;
|
||||
padding: $default-spacer;
|
||||
}
|
||||
|
||||
th.text-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
&.horizontal {
|
||||
th {
|
||||
border-bottom: 1px solid $grey;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -241,6 +241,7 @@
|
|||
}
|
||||
|
||||
input[type=email],
|
||||
input[type=password],
|
||||
input[type=number],
|
||||
input[type=tel], {
|
||||
max-width: 500px;
|
||||
|
@ -356,6 +357,17 @@
|
|||
}
|
||||
}
|
||||
|
||||
.cnaf-inputs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
max-width: 700px;
|
||||
|
||||
input {
|
||||
width: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
input.aa-input,
|
||||
input.aa-hint {
|
||||
border-radius: 4px;
|
||||
|
|
12
app/assets/stylesheets/france-connect-informations.scss
Normal file
12
app/assets/stylesheets/france-connect-informations.scss
Normal file
|
@ -0,0 +1,12 @@
|
|||
@import "constants";
|
||||
|
||||
.france-connect-informations.card {
|
||||
width: 100%;
|
||||
padding-top: $default-spacer;
|
||||
padding-bottom: $default-spacer;
|
||||
}
|
||||
|
||||
.france-connect-informations-logo img {
|
||||
width: 100px;
|
||||
margin-right: $default-padding;
|
||||
}
|
|
@ -85,6 +85,10 @@ class ApplicationController < ActionController::Base
|
|||
end
|
||||
end
|
||||
|
||||
def ajax_redirect(path)
|
||||
"window.location.href='#{path}'"
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def feature_enabled?(feature_name)
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
class FranceConnect::ParticulierController < ApplicationController
|
||||
before_action :redirect_to_login_if_fc_aborted, only: [:callback]
|
||||
before_action :securely_retrieve_fci, only: [:merge, :merge_with_existing_account, :merge_with_new_account]
|
||||
|
||||
def login
|
||||
if FranceConnectService.enabled?
|
||||
|
@ -11,23 +12,88 @@ class FranceConnect::ParticulierController < ApplicationController
|
|||
|
||||
def callback
|
||||
fci = FranceConnectService.find_or_retrieve_france_connect_information(params[:code])
|
||||
fci.associate_user!
|
||||
|
||||
if fci.user && !fci.user.can_france_connect?
|
||||
fci.destroy
|
||||
redirect_to new_user_session_path, alert: t('errors.messages.france_connect.forbidden_html', reset_link: new_user_password_path)
|
||||
return
|
||||
if fci.user.nil?
|
||||
preexisting_unlinked_user = User.find_by(email: fci.email_france_connect.downcase)
|
||||
|
||||
if preexisting_unlinked_user.nil?
|
||||
fci.associate_user!(fci.email_france_connect)
|
||||
connect_france_connect_particulier(fci.user)
|
||||
else
|
||||
redirect_to france_connect_particulier_merge_path(fci.create_merge_token!)
|
||||
end
|
||||
else
|
||||
user = fci.user
|
||||
|
||||
if user.can_france_connect?
|
||||
fci.update(updated_at: Time.zone.now)
|
||||
connect_france_connect_particulier(user)
|
||||
else
|
||||
fci.destroy
|
||||
redirect_to new_user_session_path, alert: t('errors.messages.france_connect.forbidden_html', reset_link: new_user_password_path)
|
||||
end
|
||||
end
|
||||
|
||||
connect_france_connect_particulier(fci.user)
|
||||
|
||||
rescue Rack::OAuth2::Client::Error => e
|
||||
Rails.logger.error e.message
|
||||
redirect_france_connect_error_connection
|
||||
end
|
||||
|
||||
def merge
|
||||
end
|
||||
|
||||
def merge_with_existing_account
|
||||
user = User.find_by(email: sanitized_email_params)
|
||||
|
||||
if user.valid_for_authentication? { user.valid_password?(password_params) }
|
||||
if !user.can_france_connect?
|
||||
flash.alert = "#{user.email} ne peut utiliser FranceConnect"
|
||||
|
||||
render js: ajax_redirect(root_path)
|
||||
else
|
||||
@fci.update(user: user)
|
||||
@fci.delete_merge_token!
|
||||
|
||||
flash.notice = "Les comptes FranceConnect et #{APPLICATION_NAME} sont à présent fusionnés"
|
||||
connect_france_connect_particulier(user)
|
||||
end
|
||||
else
|
||||
flash.alert = 'Mauvais mot de passe'
|
||||
|
||||
render js: helpers.render_flash
|
||||
end
|
||||
end
|
||||
|
||||
def merge_with_new_account
|
||||
user = User.find_by(email: sanitized_email_params)
|
||||
|
||||
if user.nil?
|
||||
@fci.associate_user!(sanitized_email_params)
|
||||
@fci.delete_merge_token!
|
||||
|
||||
flash.notice = "Les comptes FranceConnect et #{APPLICATION_NAME} sont à présent fusionnés"
|
||||
connect_france_connect_particulier(@fci.user)
|
||||
else
|
||||
@email = sanitized_email_params
|
||||
@merge_token = merge_token_params
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def securely_retrieve_fci
|
||||
@fci = FranceConnectInformation.find_by(merge_token: merge_token_params)
|
||||
|
||||
if @fci.nil? || !@fci.valid_for_merge?
|
||||
flash.alert = 'Votre compte FranceConnect a expiré, veuillez recommencer.'
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to root_path }
|
||||
format.js { render js: ajax_redirect(root_path) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def redirect_to_login_if_fc_aborted
|
||||
if params[:code].blank?
|
||||
redirect_to new_user_session_path
|
||||
|
@ -39,23 +105,32 @@ class FranceConnect::ParticulierController < ApplicationController
|
|||
sign_out :user
|
||||
end
|
||||
|
||||
if instructeur_signed_in?
|
||||
sign_out :instructeur
|
||||
end
|
||||
|
||||
if administrateur_signed_in?
|
||||
sign_out :administrateur
|
||||
end
|
||||
|
||||
sign_in user
|
||||
|
||||
user.update_attribute('loged_in_with_france_connect', User.loged_in_with_france_connects.fetch(:particulier))
|
||||
|
||||
redirect_to stored_location_for(current_user) || root_path(current_user)
|
||||
redirection_location = stored_location_for(current_user) || root_path(current_user)
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to redirection_location }
|
||||
format.js { render js: ajax_redirect(root_path) }
|
||||
end
|
||||
end
|
||||
|
||||
def redirect_france_connect_error_connection
|
||||
flash.alert = t('errors.messages.france_connect.connexion')
|
||||
redirect_to(new_user_session_path)
|
||||
end
|
||||
|
||||
def merge_token_params
|
||||
params[:merge_token]
|
||||
end
|
||||
|
||||
def password_params
|
||||
params[:password]
|
||||
end
|
||||
|
||||
def sanitized_email_params
|
||||
params[:email]&.gsub(/[[:space:]]/, ' ')&.strip&.downcase
|
||||
end
|
||||
end
|
||||
|
|
|
@ -337,8 +337,8 @@ module Users
|
|||
def champs_params
|
||||
params.permit(dossier: {
|
||||
champs_attributes: [
|
||||
:id, :value, :external_id, :primary_value, :secondary_value, :piece_justificative_file, value: [],
|
||||
champs_attributes: [:id, :_destroy, :value, :external_id, :primary_value, :secondary_value, :piece_justificative_file, value: []]
|
||||
:id, :value, :external_id, :primary_value, :secondary_value, :numero_allocataire, :code_postal, :piece_justificative_file, value: [],
|
||||
champs_attributes: [:id, :_destroy, :value, :external_id, :primary_value, :secondary_value, :numero_allocataire, :code_postal, :piece_justificative_file, value: []]
|
||||
]
|
||||
})
|
||||
end
|
||||
|
@ -382,7 +382,8 @@ module Users
|
|||
if @dossier.champs.any?(&:changed_for_autosave?)
|
||||
@dossier.last_champ_updated_at = Time.zone.now
|
||||
end
|
||||
if !@dossier.save
|
||||
|
||||
if !@dossier.save(**validation_options)
|
||||
errors += @dossier.errors.full_messages
|
||||
elsif change_groupe_instructeur?
|
||||
@dossier.assign_to_groupe_instructeur(groupe_instructeur_from_params)
|
||||
|
@ -453,5 +454,16 @@ module Users
|
|||
def save_draft?
|
||||
dossier.brouillon? && !params[:submit_draft]
|
||||
end
|
||||
|
||||
def validation_options
|
||||
if save_draft?
|
||||
{ context: :brouillon }
|
||||
else
|
||||
# rubocop:disable Lint/BooleanSymbol
|
||||
# Force ActiveRecord to re-validate associated records.
|
||||
{ context: :false }
|
||||
# rubocop:enable Lint/BooleanSymbol
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1869,6 +1869,11 @@ enum TypeDeChamp {
|
|||
"""
|
||||
civilite
|
||||
|
||||
"""
|
||||
Données de la Caisse nationale des allocations familiales
|
||||
"""
|
||||
cnaf
|
||||
|
||||
"""
|
||||
Communes
|
||||
"""
|
||||
|
|
|
@ -13,7 +13,7 @@ module ProcedureHelper
|
|||
safe_join(parts, ' ')
|
||||
end
|
||||
|
||||
def procedure_publish_text(procedure, key)
|
||||
def procedure_publish_label(procedure, key)
|
||||
# i18n-tasks-use t('modal.publish.body.publish')
|
||||
# i18n-tasks-use t('modal.publish.body.reopen')
|
||||
# i18n-tasks-use t('modal.publish.submit.publish')
|
||||
|
|
19
app/javascript/new_design/fc-fusion.js
Normal file
19
app/javascript/new_design/fc-fusion.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
import { show, hide } from '@utils';
|
||||
|
||||
export function showFusion() {
|
||||
show(document.querySelector('.fusion'));
|
||||
hide(document.querySelector('.new-account'));
|
||||
hide(document.querySelector('.new-account-password-confirmation'));
|
||||
}
|
||||
|
||||
export function showNewAccount() {
|
||||
hide(document.querySelector('.fusion'));
|
||||
show(document.querySelector('.new-account'));
|
||||
hide(document.querySelector('.new-account-password-confirmation'));
|
||||
}
|
||||
|
||||
export function showNewAccountPasswordConfirmation() {
|
||||
hide(document.querySelector('.fusion'));
|
||||
hide(document.querySelector('.new-account'));
|
||||
show(document.querySelector('.new-account-password-confirmation'));
|
||||
}
|
|
@ -41,6 +41,11 @@ import {
|
|||
acceptEmailSuggestion,
|
||||
discardEmailSuggestionBox
|
||||
} from '../new_design/user-sign_up';
|
||||
import {
|
||||
showFusion,
|
||||
showNewAccount,
|
||||
showNewAccountPasswordConfirmation
|
||||
} from '../new_design/fc-fusion';
|
||||
|
||||
// This is the global application namespace where we expose helpers used from rails views
|
||||
const DS = {
|
||||
|
@ -49,6 +54,9 @@ const DS = {
|
|||
showMotivation,
|
||||
motivationCancel,
|
||||
showImportJustificatif,
|
||||
showFusion,
|
||||
showNewAccount,
|
||||
showNewAccountPasswordConfirmation,
|
||||
replaceSemicolonByComma,
|
||||
acceptEmailSuggestion,
|
||||
discardEmailSuggestionBox
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
require 'active_job/logging'
|
||||
require 'logstash-event'
|
||||
|
||||
class ActiveJobLogSubscriber < ::ActiveJob::LogSubscriber
|
||||
class ActiveJob::ApplicationLogSubscriber < ::ActiveJob::LogSubscriber
|
||||
def enqueue(event)
|
||||
process_event(event, 'enqueue')
|
||||
end
|
|
@ -1,4 +1,4 @@
|
|||
class APIParticulier::CNAFAdapter
|
||||
class APIParticulier::CnafAdapter
|
||||
class InvalidSchemaError < ::StandardError
|
||||
def initialize(errors)
|
||||
super(errors.map(&:to_json).join("\n"))
|
||||
|
|
|
@ -14,7 +14,7 @@ module APIParticulier
|
|||
msg = <<~TEXT
|
||||
url: #{url}
|
||||
HTTP error code: #{http_error_code}
|
||||
#{response.body}
|
||||
#{response.body.force_encoding('UTF-8')}
|
||||
curl message: #{curl_message}
|
||||
total time: #{total_time}
|
||||
connect time: #{connect_time}
|
||||
|
|
|
@ -8,6 +8,7 @@ module APIParticulier
|
|||
def available_sources
|
||||
@procedure.api_particulier_scopes
|
||||
.map { |provider_and_scope| raw_scopes[provider_and_scope] }
|
||||
.compact
|
||||
.map { |provider, scope| extract_sources(provider, scope) }
|
||||
.reduce({}) { |acc, el| acc.deep_merge(el) }
|
||||
end
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
# row :integer
|
||||
# type :string
|
||||
# value :string
|
||||
# value_json :jsonb
|
||||
# created_at :datetime
|
||||
# updated_at :datetime
|
||||
# dossier_id :integer
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
# row :integer
|
||||
# type :string
|
||||
# value :string
|
||||
# value_json :jsonb
|
||||
# created_at :datetime
|
||||
# updated_at :datetime
|
||||
# dossier_id :integer
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
# row :integer
|
||||
# type :string
|
||||
# value :string
|
||||
# value_json :jsonb
|
||||
# created_at :datetime
|
||||
# updated_at :datetime
|
||||
# dossier_id :integer
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
# row :integer
|
||||
# type :string
|
||||
# value :string
|
||||
# value_json :jsonb
|
||||
# created_at :datetime
|
||||
# updated_at :datetime
|
||||
# dossier_id :integer
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
# row :integer
|
||||
# type :string
|
||||
# value :string
|
||||
# value_json :jsonb
|
||||
# created_at :datetime
|
||||
# updated_at :datetime
|
||||
# dossier_id :integer
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
# row :integer
|
||||
# type :string
|
||||
# value :string
|
||||
# value_json :jsonb
|
||||
# created_at :datetime
|
||||
# updated_at :datetime
|
||||
# dossier_id :integer
|
||||
|
|
52
app/models/champs/cnaf_champ.rb
Normal file
52
app/models/champs/cnaf_champ.rb
Normal file
|
@ -0,0 +1,52 @@
|
|||
# == Schema Information
|
||||
#
|
||||
# Table name: champs
|
||||
#
|
||||
# id :integer not null, primary key
|
||||
# data :jsonb
|
||||
# fetch_external_data_exceptions :string is an Array
|
||||
# private :boolean default(FALSE), not null
|
||||
# row :integer
|
||||
# type :string
|
||||
# value :string
|
||||
# value_json :jsonb
|
||||
# created_at :datetime
|
||||
# updated_at :datetime
|
||||
# dossier_id :integer
|
||||
# etablissement_id :integer
|
||||
# external_id :string
|
||||
# parent_id :bigint
|
||||
# type_de_champ_id :integer
|
||||
#
|
||||
class Champs::CnafChamp < Champs::TextChamp
|
||||
# see https://github.com/betagouv/api-particulier/blob/master/src/presentation/middlewares/cnaf-input-validation.middleware.ts
|
||||
validates :numero_allocataire, format: { with: /\A\d{1,7}\z/ }, if: -> { code_postal.present? && validation_context != :brouillon }
|
||||
validates :code_postal, format: { with: /\A\w{5}\z/ }, if: -> { numero_allocataire.present? && validation_context != :brouillon }
|
||||
|
||||
store_accessor :value_json, :numero_allocataire, :code_postal
|
||||
|
||||
def blank?
|
||||
external_id.nil?
|
||||
end
|
||||
|
||||
def fetch_external_data?
|
||||
true
|
||||
end
|
||||
|
||||
def fetch_external_data
|
||||
if valid?
|
||||
APIParticulier::CnafAdapter.new(
|
||||
procedure.api_particulier_token,
|
||||
numero_allocataire,
|
||||
code_postal,
|
||||
procedure.api_particulier_sources
|
||||
).to_params
|
||||
end
|
||||
end
|
||||
|
||||
def external_id
|
||||
if numero_allocataire.present? && code_postal.present?
|
||||
{ code_postal: code_postal, numero_allocataire: numero_allocataire }.to_json
|
||||
end
|
||||
end
|
||||
end
|
|
@ -9,6 +9,7 @@
|
|||
# row :integer
|
||||
# type :string
|
||||
# value :string
|
||||
# value_json :jsonb
|
||||
# created_at :datetime
|
||||
# updated_at :datetime
|
||||
# dossier_id :integer
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
# row :integer
|
||||
# type :string
|
||||
# value :string
|
||||
# value_json :jsonb
|
||||
# created_at :datetime
|
||||
# updated_at :datetime
|
||||
# dossier_id :integer
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
# row :integer
|
||||
# type :string
|
||||
# value :string
|
||||
# value_json :jsonb
|
||||
# created_at :datetime
|
||||
# updated_at :datetime
|
||||
# dossier_id :integer
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
# row :integer
|
||||
# type :string
|
||||
# value :string
|
||||
# value_json :jsonb
|
||||
# created_at :datetime
|
||||
# updated_at :datetime
|
||||
# dossier_id :integer
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
# row :integer
|
||||
# type :string
|
||||
# value :string
|
||||
# value_json :jsonb
|
||||
# created_at :datetime
|
||||
# updated_at :datetime
|
||||
# dossier_id :integer
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
# row :integer
|
||||
# type :string
|
||||
# value :string
|
||||
# value_json :jsonb
|
||||
# created_at :datetime
|
||||
# updated_at :datetime
|
||||
# dossier_id :integer
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
# row :integer
|
||||
# type :string
|
||||
# value :string
|
||||
# value_json :jsonb
|
||||
# created_at :datetime
|
||||
# updated_at :datetime
|
||||
# dossier_id :integer
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
# row :integer
|
||||
# type :string
|
||||
# value :string
|
||||
# value_json :jsonb
|
||||
# created_at :datetime
|
||||
# updated_at :datetime
|
||||
# dossier_id :integer
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
# row :integer
|
||||
# type :string
|
||||
# value :string
|
||||
# value_json :jsonb
|
||||
# created_at :datetime
|
||||
# updated_at :datetime
|
||||
# dossier_id :integer
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
# row :integer
|
||||
# type :string
|
||||
# value :string
|
||||
# value_json :jsonb
|
||||
# created_at :datetime
|
||||
# updated_at :datetime
|
||||
# dossier_id :integer
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
# row :integer
|
||||
# type :string
|
||||
# value :string
|
||||
# value_json :jsonb
|
||||
# created_at :datetime
|
||||
# updated_at :datetime
|
||||
# dossier_id :integer
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
# row :integer
|
||||
# type :string
|
||||
# value :string
|
||||
# value_json :jsonb
|
||||
# created_at :datetime
|
||||
# updated_at :datetime
|
||||
# dossier_id :integer
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
# row :integer
|
||||
# type :string
|
||||
# value :string
|
||||
# value_json :jsonb
|
||||
# created_at :datetime
|
||||
# updated_at :datetime
|
||||
# dossier_id :integer
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
# row :integer
|
||||
# type :string
|
||||
# value :string
|
||||
# value_json :jsonb
|
||||
# created_at :datetime
|
||||
# updated_at :datetime
|
||||
# dossier_id :integer
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
# row :integer
|
||||
# type :string
|
||||
# value :string
|
||||
# value_json :jsonb
|
||||
# created_at :datetime
|
||||
# updated_at :datetime
|
||||
# dossier_id :integer
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
# row :integer
|
||||
# type :string
|
||||
# value :string
|
||||
# value_json :jsonb
|
||||
# created_at :datetime
|
||||
# updated_at :datetime
|
||||
# dossier_id :integer
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
# row :integer
|
||||
# type :string
|
||||
# value :string
|
||||
# value_json :jsonb
|
||||
# created_at :datetime
|
||||
# updated_at :datetime
|
||||
# dossier_id :integer
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
# row :integer
|
||||
# type :string
|
||||
# value :string
|
||||
# value_json :jsonb
|
||||
# created_at :datetime
|
||||
# updated_at :datetime
|
||||
# dossier_id :integer
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
# row :integer
|
||||
# type :string
|
||||
# value :string
|
||||
# value_json :jsonb
|
||||
# created_at :datetime
|
||||
# updated_at :datetime
|
||||
# dossier_id :integer
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
# row :integer
|
||||
# type :string
|
||||
# value :string
|
||||
# value_json :jsonb
|
||||
# created_at :datetime
|
||||
# updated_at :datetime
|
||||
# dossier_id :integer
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
# row :integer
|
||||
# type :string
|
||||
# value :string
|
||||
# value_json :jsonb
|
||||
# created_at :datetime
|
||||
# updated_at :datetime
|
||||
# dossier_id :integer
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
# row :integer
|
||||
# type :string
|
||||
# value :string
|
||||
# value_json :jsonb
|
||||
# created_at :datetime
|
||||
# updated_at :datetime
|
||||
# dossier_id :integer
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
# row :integer
|
||||
# type :string
|
||||
# value :string
|
||||
# value_json :jsonb
|
||||
# created_at :datetime
|
||||
# updated_at :datetime
|
||||
# dossier_id :integer
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
# row :integer
|
||||
# type :string
|
||||
# value :string
|
||||
# value_json :jsonb
|
||||
# created_at :datetime
|
||||
# updated_at :datetime
|
||||
# dossier_id :integer
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
# row :integer
|
||||
# type :string
|
||||
# value :string
|
||||
# value_json :jsonb
|
||||
# created_at :datetime
|
||||
# updated_at :datetime
|
||||
# dossier_id :integer
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
# row :integer
|
||||
# type :string
|
||||
# value :string
|
||||
# value_json :jsonb
|
||||
# created_at :datetime
|
||||
# updated_at :datetime
|
||||
# dossier_id :integer
|
||||
|
|
|
@ -38,6 +38,8 @@ class Expert < ApplicationRecord
|
|||
end
|
||||
|
||||
def merge(old_expert)
|
||||
return if old_expert.nil?
|
||||
|
||||
procedure_with_new, procedure_without_new = old_expert
|
||||
.procedures
|
||||
.partition { |p| p.experts.exists?(id) }
|
||||
|
|
|
@ -10,35 +10,50 @@
|
|||
# family_name :string
|
||||
# gender :string
|
||||
# given_name :string
|
||||
# merge_token :string
|
||||
# merge_token_created_at :datetime
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# france_connect_particulier_id :string
|
||||
# user_id :integer not null
|
||||
# user_id :integer
|
||||
#
|
||||
class FranceConnectInformation < ApplicationRecord
|
||||
MERGE_VALIDITY = 15.minutes
|
||||
|
||||
belongs_to :user, optional: true
|
||||
|
||||
validates :france_connect_particulier_id, presence: true, allow_blank: false, allow_nil: false
|
||||
|
||||
def associate_user!
|
||||
user = User.find_by(email: email_france_connect.downcase)
|
||||
|
||||
if user.nil?
|
||||
begin
|
||||
user = User.create!(
|
||||
email: email_france_connect.downcase,
|
||||
password: Devise.friendly_token[0, 20],
|
||||
confirmed_at: Time.zone.now
|
||||
)
|
||||
rescue ActiveRecord::RecordNotUnique
|
||||
# ignore this exception because we check before is user is nil.
|
||||
# exception can be raised in race conditions, when FranceConnect calls callback 2 times.
|
||||
# At the 2nd call, user is nil but exception is raised at the creation of the user
|
||||
# because the first call has already created a user
|
||||
end
|
||||
def associate_user!(email)
|
||||
begin
|
||||
user = User.create!(
|
||||
email: email.downcase,
|
||||
password: Devise.friendly_token[0, 20],
|
||||
confirmed_at: Time.zone.now
|
||||
)
|
||||
rescue ActiveRecord::RecordNotUnique
|
||||
# ignore this exception because we check before is user is nil.
|
||||
# exception can be raised in race conditions, when FranceConnect calls callback 2 times.
|
||||
# At the 2nd call, user is nil but exception is raised at the creation of the user
|
||||
# because the first call has already created a user
|
||||
end
|
||||
|
||||
update_attribute('user_id', user.id)
|
||||
touch # needed to update updated_at column
|
||||
end
|
||||
|
||||
def create_merge_token!
|
||||
merge_token = SecureRandom.uuid
|
||||
update(merge_token: merge_token, merge_token_created_at: Time.zone.now)
|
||||
|
||||
merge_token
|
||||
end
|
||||
|
||||
def valid_for_merge?
|
||||
(MERGE_VALIDITY.ago < merge_token_created_at) && user_id.nil?
|
||||
end
|
||||
|
||||
def delete_merge_token!
|
||||
update(merge_token: nil, merge_token_created_at: nil)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -252,6 +252,8 @@ class Instructeur < ApplicationRecord
|
|||
end
|
||||
|
||||
def merge(old_instructeur)
|
||||
return if old_instructeur.nil?
|
||||
|
||||
old_instructeur
|
||||
.assign_to
|
||||
.where.not(groupe_instructeur_id: assign_to.pluck(:groupe_instructeur_id))
|
||||
|
|
|
@ -716,6 +716,10 @@ class Procedure < ApplicationRecord
|
|||
published_revision.touch(:published_at)
|
||||
end
|
||||
|
||||
def cnaf_enabled?
|
||||
api_particulier_sources['cnaf'].present?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def before_publish
|
||||
|
|
|
@ -48,7 +48,8 @@ class TypeDeChamp < ApplicationRecord
|
|||
repetition: 'repetition',
|
||||
titre_identite: 'titre_identite',
|
||||
iban: 'iban',
|
||||
annuaire_education: 'annuaire_education'
|
||||
annuaire_education: 'annuaire_education',
|
||||
cnaf: 'cnaf'
|
||||
}
|
||||
|
||||
belongs_to :revision, class_name: 'ProcedureRevision', optional: true
|
||||
|
@ -291,17 +292,26 @@ class TypeDeChamp < ApplicationRecord
|
|||
def self.type_de_champ_types_for(procedure, user)
|
||||
has_legacy_number = (procedure.types_de_champ + procedure.types_de_champ_private).any?(&:legacy_number?)
|
||||
|
||||
show_number = -> (tdc) { tdc != TypeDeChamp.type_champs.fetch(:number) || has_legacy_number }
|
||||
|
||||
enabled_featured_champ = -> (tdc) do
|
||||
filter_featured_tdc = -> (tdc) do
|
||||
feature_name = FEATURE_FLAGS[tdc]
|
||||
feature_name.blank? || Flipper.enabled?(feature_name, user)
|
||||
end
|
||||
|
||||
filter_tdc = -> (tdc) do
|
||||
case tdc
|
||||
when TypeDeChamp.type_champs.fetch(:number)
|
||||
has_legacy_number
|
||||
when TypeDeChamp.type_champs.fetch(:cnaf)
|
||||
procedure.cnaf_enabled?
|
||||
else
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
type_champs
|
||||
.keys
|
||||
.filter(&show_number)
|
||||
.filter(&enabled_featured_champ)
|
||||
.filter(&filter_tdc)
|
||||
.filter(&filter_featured_tdc)
|
||||
.map { |tdc| [I18n.t("activerecord.attributes.type_de_champ.type_champs.#{tdc}"), tdc] }
|
||||
.sort_by(&:first)
|
||||
end
|
||||
|
|
2
app/models/types_de_champ/cnaf_type_de_champ.rb
Normal file
2
app/models/types_de_champ/cnaf_type_de_champ.rb
Normal file
|
@ -0,0 +1,2 @@
|
|||
class TypesDeChamp::CnafTypeDeChamp < TypesDeChamp::TextTypeDeChamp
|
||||
end
|
|
@ -0,0 +1,12 @@
|
|||
%p
|
||||
Le compte #{email} existe déjà sur #{APPLICATION_NAME}
|
||||
%br
|
||||
entrez votre mot de passe pour fusionner les comptes
|
||||
|
||||
= form_tag france_connect_particulier_merge_with_existing_account_path, remote: true, class: 'mt-2 form' do
|
||||
= hidden_field_tag :merge_token, merge_token
|
||||
= hidden_field_tag :email, email
|
||||
= label_tag :password, 'Mot de passe (8 caractères minimum)'
|
||||
= password_field_tag :password, nil, autocomplete: 'current-password', id: 'password-for-another-account'
|
||||
= button_tag 'revenir en arrière', type: 'button', class: 'button secondary', onclick: 'DS.showNewAccount(event);'
|
||||
= submit_tag 'Fusionner les comptes', class: 'button primary'
|
44
app/views/france_connect/particulier/merge.html.haml
Normal file
44
app/views/france_connect/particulier/merge.html.haml
Normal file
|
@ -0,0 +1,44 @@
|
|||
= content_for :title, "Fusion des comptes FC et #{APPLICATION_NAME}"
|
||||
|
||||
.container
|
||||
%h1.page-title Fusion des comptes FranceConnect et #{APPLICATION_NAME}
|
||||
|
||||
%p
|
||||
Bonjour,
|
||||
%br
|
||||
%br
|
||||
Votre compte FranceConnect utilise <b class='bold'>#{@fci.email_france_connect}</b> comme email de contact.
|
||||
%br
|
||||
Or il existe un compte sur #{APPLICATION_NAME} avec cet email.
|
||||
|
||||
.form.mt-2
|
||||
%label Ce compte #{@fci.email_france_connect} vous appartient-il ?
|
||||
%fieldset.radios
|
||||
%label{ onclick: "DS.showFusion(event);" }
|
||||
= radio_button_tag :value, true, false, autocomplete: "off", id: 'it-is-mine'
|
||||
Oui
|
||||
|
||||
%label{ onclick: "DS.showNewAccount(event);" }
|
||||
= radio_button_tag :value, false, false, autocomplete: "off", id: 'it-is-not-mine'
|
||||
Non
|
||||
|
||||
.fusion.hidden
|
||||
%p Pour les fusionner, entrez votre mot de passe
|
||||
|
||||
= form_tag france_connect_particulier_merge_with_existing_account_path, remote: true, class: 'mt-2 form' do
|
||||
= hidden_field_tag :merge_token, @fci.merge_token
|
||||
= hidden_field_tag :email, @fci.email_france_connect
|
||||
= label_tag :password, 'Mot de passe (8 caractères minimum)'
|
||||
= password_field_tag :password, nil, autocomplete: 'current-password'
|
||||
= submit_tag 'Fusionner les comptes', class: 'button primary'
|
||||
|
||||
.new-account.hidden
|
||||
%p Donnez-nous alors le mail que #{APPLICATION_NAME} utilisera pour vous contacter
|
||||
|
||||
= form_tag france_connect_particulier_merge_with_new_account_path, remote: true, class: 'mt-2 form' do
|
||||
= hidden_field_tag :merge_token, @fci.merge_token
|
||||
= label_tag :email, 'Email (nom@site.com)'
|
||||
= email_field_tag :email
|
||||
= submit_tag 'Utiliser ce mail', class: 'button primary'
|
||||
|
||||
.new-account-password-confirmation.hidden
|
|
@ -0,0 +1,3 @@
|
|||
<%= render_to_element('.new-account-password-confirmation', partial: 'password_confirmation', locals: { email: @email, merge_token: @merge_token }) %>
|
||||
|
||||
DS.showNewAccountPasswordConfirmation();
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
.container
|
||||
.flex
|
||||
= link_to admin_procedure_api_particulier_jeton_path, class: 'card-admin' do
|
||||
= link_to admin_procedure_api_particulier_jeton_path, class: 'card-admin', id: 'add-jeton' do
|
||||
- if @procedure.api_particulier_token.blank?
|
||||
%div
|
||||
%span.icon.clock
|
||||
|
|
|
@ -40,4 +40,4 @@
|
|||
- if errors.details[:path].present?
|
||||
- options[:disabled] = :disabled
|
||||
.flex.justify-end
|
||||
= submit_tag procedure_publish_text(procedure, :submit), options
|
||||
= submit_tag procedure_publish_label(procedure, :submit), options
|
||||
|
|
|
@ -193,7 +193,7 @@
|
|||
%p.button Modifier
|
||||
|
||||
- if feature_enabled?(:api_particulier)
|
||||
= link_to admin_procedure_api_particulier_path(@procedure), class: 'card-admin' do
|
||||
= link_to admin_procedure_api_particulier_path(@procedure), class: 'card-admin', id: 'api-particulier' do
|
||||
- if @procedure.api_particulier_token.present?
|
||||
%div
|
||||
%span.icon.accept
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
- scopes.each do |scope_key, sources|
|
||||
%h3.explication-libelle= t("api_particulier.providers.#{provider_key}.scopes.#{scope_key}.libelle")
|
||||
%ul.procedure-admin-api-particulier-sources
|
||||
%ul.procedure-admin-api-particulier-sources{ id: scope_key }
|
||||
- sources.each do |source_key, enabled_hash|
|
||||
- enabled = (@procedure.api_particulier_sources.dig(provider_key, scope_key)&.include?(source_key)).present?
|
||||
%li
|
||||
|
|
10
app/views/shared/champs/cnaf/_adresse.html.haml
Normal file
10
app/views/shared/champs/cnaf/_adresse.html.haml
Normal file
|
@ -0,0 +1,10 @@
|
|||
%table.cnaf
|
||||
%caption #{t("api_particulier.providers.cnaf.scopes.adresse.libelle")} :
|
||||
- for key in ['identite', 'complementIdentite', 'numeroRue', 'complementIdentiteGeo', 'lieuDit', 'codePostalVille', 'pays'] do
|
||||
- if adresse[key].present?
|
||||
%tr
|
||||
%th= t("api_particulier.providers.cnaf.scopes.adresse.#{key}")
|
||||
%td= adresse[key]
|
||||
|
||||
|
||||
|
19
app/views/shared/champs/cnaf/_personnes.html.haml
Normal file
19
app/views/shared/champs/cnaf/_personnes.html.haml
Normal file
|
@ -0,0 +1,19 @@
|
|||
%table.cnaf.horizontal
|
||||
%caption #{t("api_particulier.providers.cnaf.scopes.#{scope}.libelle")} :
|
||||
%thead
|
||||
%tr
|
||||
- for key in ['nomPrenom', 'sexe', 'dateDeNaissance'] do
|
||||
- if personnes.first[key].present?
|
||||
%th{ class: "#{"text-right" if key == 'dateDeNaissance'}" }= t("api_particulier.providers.cnaf.scopes.personne.#{key}")
|
||||
%tbody
|
||||
- personnes.each do |personne|
|
||||
%tr
|
||||
- for key in ['nomPrenom', 'sexe', 'dateDeNaissance'] do
|
||||
- if personne[key].present?
|
||||
- case key
|
||||
- when 'dateDeNaissance'
|
||||
%td.text-right= try_format_datetime(Date.strptime(personne[key], "%d%m%Y"))
|
||||
- when 'sexe'
|
||||
%td= t("api_particulier.providers.cnaf.scopes.personne.#{personne[key]}")
|
||||
- else
|
||||
%td= personne[key]
|
14
app/views/shared/champs/cnaf/_quotient_familial.html.haml
Normal file
14
app/views/shared/champs/cnaf/_quotient_familial.html.haml
Normal file
|
@ -0,0 +1,14 @@
|
|||
%table.cnaf.horizontal
|
||||
%caption #{t("api_particulier.providers.cnaf.scopes.quotient_familial.libelle")} :
|
||||
%thead
|
||||
%tr
|
||||
- for key in ['quotientFamilial', 'mois', 'annee'] do
|
||||
- if quotient_familial[key].present?
|
||||
%th.text-right= t("api_particulier.providers.cnaf.scopes.quotient_familial.#{key}")
|
||||
%tbody
|
||||
%tr
|
||||
- for key in ['quotientFamilial', 'mois', 'annee'] do
|
||||
- if quotient_familial[key].present?
|
||||
%td.text-right= quotient_familial[key]
|
||||
- else
|
||||
%td
|
24
app/views/shared/champs/cnaf/_show.html.haml
Normal file
24
app/views/shared/champs/cnaf/_show.html.haml
Normal file
|
@ -0,0 +1,24 @@
|
|||
- if champ.blank?
|
||||
%p= t('.not_filled')
|
||||
- elsif champ.data.blank?
|
||||
%p= t('.fetching_data',
|
||||
numero_allocataire: champ.numero_allocataire,
|
||||
code_postal: champ.code_postal)
|
||||
- else
|
||||
- if profile == 'usager'
|
||||
%p= t('.data_fetched',
|
||||
sources: champ.procedure.api_particulier_sources['cnaf'].keys.map(&:to_s).join(', '),
|
||||
numero_allocataire: champ.numero_allocataire,
|
||||
code_postal: champ.code_postal)
|
||||
|
||||
- if profile == 'instructeur'
|
||||
%p= t('.data_fetched_title')
|
||||
|
||||
- ['adresse', 'quotient_familial', 'enfants', 'allocataires'].each do |scope|
|
||||
- if champ.data[scope].present?
|
||||
- if scope == 'quotient_familial'
|
||||
= render partial: 'shared/champs/cnaf/quotient_familial', locals: { quotient_familial: champ.data[scope] }
|
||||
- if scope.in? ['enfants', 'allocataires']
|
||||
= render partial: 'shared/champs/cnaf/personnes', locals: { scope: scope, personnes: champ.data[scope] }
|
||||
- elsif scope == 'adresse'
|
||||
= render partial: 'shared/champs/cnaf/adresse', locals: { adresse: champ.data[scope] }
|
|
@ -36,6 +36,8 @@
|
|||
= render partial: "shared/champs/textarea/show", locals: { champ: c }
|
||||
- when TypeDeChamp.type_champs.fetch(:annuaire_education)
|
||||
= render partial: "shared/champs/annuaire_education/show", locals: { champ: c }
|
||||
- when TypeDeChamp.type_champs.fetch(:cnaf)
|
||||
= render partial: "shared/champs/cnaf/show", locals: { champ: c, profile: profile }
|
||||
- when TypeDeChamp.type_champs.fetch(:address)
|
||||
= render partial: "shared/champs/address/show", locals: { champ: c }
|
||||
- when TypeDeChamp.type_champs.fetch(:communes)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
.card.featured
|
||||
.flex.justify-center
|
||||
= image_tag "logo-france-connect.png", alt: "France Connect logo", width: 200, class: "mb-2"
|
||||
.card-title
|
||||
.france-connect-informations.card.featured.flex.align-center
|
||||
.france-connect-informations-logo
|
||||
= image_tag "logo-france-connect.png", alt: "France Connect logo"
|
||||
%div
|
||||
- if user_information.updated_at.present?
|
||||
Le dossier a été déposé par le compte de #{user_information&.given_name} #{user_information&.family_name}, authentifié par France Connect le #{user_information.updated_at.strftime('%d/%m/%Y')}.
|
||||
- else
|
||||
|
|
16
app/views/shared/dossiers/editable_champs/_cnaf.html.haml
Normal file
16
app/views/shared/dossiers/editable_champs/_cnaf.html.haml
Normal file
|
@ -0,0 +1,16 @@
|
|||
.cnaf-inputs
|
||||
%div
|
||||
= form.label :numero_allocataire, t('.numero_allocataire_label')
|
||||
%p.notice= t('.numero_allocataire_notice')
|
||||
= form.text_field :numero_allocataire,
|
||||
required: champ.mandatory?,
|
||||
size: 7,
|
||||
aria: { describedby: describedby_id(champ) }
|
||||
|
||||
%div
|
||||
= form.label :code_postal, t('.code_postal_label')
|
||||
%p.notice= t('.code_postal_notice')
|
||||
= form.text_field :code_postal,
|
||||
size: 5,
|
||||
required: champ.mandatory?,
|
||||
aria: { describedby: describedby_id(champ) }
|
|
@ -2,5 +2,3 @@
|
|||
{ required: champ.mandatory? },
|
||||
'on',
|
||||
'off'
|
||||
|
||||
%br
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
%fieldset.radios
|
||||
%legend.mandatory-explanation
|
||||
Sélectionnez une des deux valeurs
|
||||
%label
|
||||
= form.radio_button :value, true
|
||||
Oui
|
||||
|
|
|
@ -14,7 +14,6 @@ ActiveSupport::Inflector.inflections(:en) do |inflect|
|
|||
inflect.acronym 'JSON'
|
||||
inflect.acronym 'RNA'
|
||||
inflect.acronym 'URL'
|
||||
inflect.acronym 'CNAF'
|
||||
inflect.irregular 'type_de_champ', 'types_de_champ'
|
||||
inflect.irregular 'type_de_champ_private', 'types_de_champ_private'
|
||||
inflect.irregular 'procedure_revision_type_de_champ', 'procedure_revision_types_de_champ'
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
require_relative './active_job_log_subscriber'
|
||||
|
||||
Rails.application.configure do
|
||||
config.lograge.formatter = Lograge::Formatters::Logstash.new
|
||||
config.lograge.base_controller_class = ['ActionController::Base', 'Manager::ApplicationController']
|
||||
|
@ -33,6 +31,6 @@ Rails.application.configure do
|
|||
config.lograge.logger = ActiveSupport::Logger.new(Rails.root.join('log', "logstash_#{Rails.env}.log"))
|
||||
|
||||
if config.lograge.enabled
|
||||
ActiveJobLogSubscriber.attach_to(:active_job)
|
||||
ActiveJob::ApplicationLogSubscriber.attach_to(:active_job)
|
||||
end
|
||||
end
|
||||
|
|
32
config/locales/api_particulier.en.yml
Normal file
32
config/locales/api_particulier.en.yml
Normal file
|
@ -0,0 +1,32 @@
|
|||
en:
|
||||
api_particulier:
|
||||
providers:
|
||||
cnaf:
|
||||
libelle: Caisse nationale d’allocations familiales (CAF)
|
||||
scopes:
|
||||
personne: &personne
|
||||
nomPrenom: first and last name
|
||||
dateDeNaissance: birth date
|
||||
sexe: sex
|
||||
M: male
|
||||
F: female
|
||||
allocataires:
|
||||
libelle: beneficiaries
|
||||
<<: *personne
|
||||
enfants:
|
||||
libelle: children
|
||||
<<: *personne
|
||||
adresse:
|
||||
libelle: address
|
||||
identite: identity
|
||||
complementIdentite: complément d’identité
|
||||
complementIdentiteGeo: complément d’identité géographique
|
||||
numeroRue: number and street
|
||||
lieuDit: lieu-dit
|
||||
codePostalVille: postcode and city
|
||||
pays: country
|
||||
quotient_familial:
|
||||
libelle: quotient familial
|
||||
quotientFamilial: quotient familial
|
||||
mois: month
|
||||
annee: year
|
|
@ -2,12 +2,14 @@ fr:
|
|||
api_particulier:
|
||||
providers:
|
||||
cnaf:
|
||||
libelle: Caisse d’allocations familiales (CAF)
|
||||
libelle: Caisse nationale d’allocations familiales (CAF)
|
||||
scopes:
|
||||
personne: &personne
|
||||
nomPrenom: noms et prénoms
|
||||
dateDeNaissance: date de naissance
|
||||
sexe: genre
|
||||
sexe: sexe
|
||||
M: masculin
|
||||
F: féminin
|
||||
allocataires:
|
||||
libelle: allocataires
|
||||
<<: *personne
|
||||
|
|
|
@ -274,6 +274,12 @@ en:
|
|||
taken: is already used for procedure. You cannot use it because it belongs to another administrator.
|
||||
# taken_can_be_claimed: est identique à celui d’une autre de vos procedures publiées. Si vous publiez cette procedure, l’ancienne sera dépubliée et ne sera plus accessible au public. Les utilisateurs qui ont commencé un brouillon vont pouvoir le déposer.
|
||||
invalid: is not valid. It must countain between 3 and 50 characters among a-z, 0-9, '_' and '-'.
|
||||
"champs/cnaf_champ":
|
||||
attributes:
|
||||
numero_allocataire:
|
||||
invalid: "must be a maximum of 7 digits"
|
||||
code_postal:
|
||||
invalid: "must be 5 characters long"
|
||||
errors:
|
||||
messages:
|
||||
dossier_not_found: "The file does not exist or you do not have access to it."
|
||||
|
|
|
@ -280,6 +280,12 @@ fr:
|
|||
taken: est déjà utilisé par une démarche. Vous ne pouvez pas l’utiliser car il appartient à un autre administrateur.
|
||||
taken_can_be_claimed: est identique à celui d’une autre de vos démarches publiées. Si vous publiez cette démarche, l’ancienne sera dépubliée et ne sera plus accessible au public. Les utilisateurs qui ont commencé un brouillon vont pouvoir le déposer.
|
||||
invalid: n’est pas valide. Il doit comporter au moins 3 caractères, au plus 50 caractères et seuls les caractères a-z, 0-9, '_' et '-' sont autorisés.
|
||||
"champs/cnaf_champ":
|
||||
attributes:
|
||||
numero_allocataire:
|
||||
invalid: "doit être composé au maximum de 7 chiffres"
|
||||
code_postal:
|
||||
invalid: "doit posséder 5 caractères"
|
||||
errors:
|
||||
messages:
|
||||
saml_not_authorized: "Vous n’êtes pas autorisé à accéder à ce service."
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
fr:
|
||||
activerecord:
|
||||
attributes:
|
||||
champ:
|
||||
value: La valeur du champ
|
||||
piece_justificative_file: La pièce justificative
|
|
@ -36,3 +36,5 @@ fr:
|
|||
titre_identite: 'Titre identité'
|
||||
iban: 'Iban'
|
||||
annuaire_education: 'Annuaire de l’éducation'
|
||||
cnaf: 'Données de la Caisse nationale des allocations familiales'
|
||||
|
||||
|
|
16
config/locales/shared.en.yml
Normal file
16
config/locales/shared.en.yml
Normal file
|
@ -0,0 +1,16 @@
|
|||
en:
|
||||
shared:
|
||||
dossiers:
|
||||
editable_champs:
|
||||
cnaf:
|
||||
numero_allocataire_label: CAF benefit number
|
||||
numero_allocataire_notice: It is usually composed of 7 digits.
|
||||
code_postal_label: postal code
|
||||
code_postal_notice: It is usually composed of 5 digits.
|
||||
champs:
|
||||
cnaf:
|
||||
show:
|
||||
not_filled: not filled
|
||||
fetching_data: "Fetching data for recipient No. %{numero_allocataire} with postal code %{code_postal}."
|
||||
data_fetched: "Data concerning %{sources} linked to the account Nº %{numero_allocataire} with the postal code %{code_postal} has been received from the CAF."
|
||||
data_fetched_title: "Data received from la Caisse nationale d’allocations familiales"
|
16
config/locales/shared.fr.yml
Normal file
16
config/locales/shared.fr.yml
Normal file
|
@ -0,0 +1,16 @@
|
|||
fr:
|
||||
shared:
|
||||
dossiers:
|
||||
editable_champs:
|
||||
cnaf:
|
||||
numero_allocataire_label: Le numéro d’allocataire CAF
|
||||
numero_allocataire_notice: Il est généralement composé de 7 chiffres.
|
||||
code_postal_label: Le code postal
|
||||
code_postal_notice: Il est généralement composé de 5 chiffres.
|
||||
champs:
|
||||
cnaf:
|
||||
show:
|
||||
not_filled: non renseigné
|
||||
fetching_data: "La récupération automatique des données pour l’allocataire Nº %{numero_allocataire} avec le code postal %{code_postal} est en cours."
|
||||
data_fetched: "Des données concernant %{sources} liées au compte Nº %{numero_allocataire} avec le code postal %{code_postal} ont été reçues depuis la CAF."
|
||||
data_fetched_title: "Données obtenues de la Caisse nationale d’allocations familiales"
|
|
@ -124,6 +124,9 @@ Rails.application.routes.draw do
|
|||
namespace :france_connect do
|
||||
get 'particulier' => 'particulier#login'
|
||||
get 'particulier/callback' => 'particulier#callback'
|
||||
get 'particulier/merge/:merge_token' => 'particulier#merge', as: :particulier_merge
|
||||
post 'particulier/merge_with_existing_account' => 'particulier#merge_with_existing_account'
|
||||
post 'particulier/merge_with_new_account' => 'particulier#merge_with_new_account'
|
||||
end
|
||||
|
||||
namespace :champs do
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
class AddValueJSONColumnToChamp < ActiveRecord::Migration[6.1]
|
||||
def change
|
||||
add_column :champs, :value_json, :jsonb
|
||||
end
|
||||
end
|
|
@ -0,0 +1,5 @@
|
|||
class RemoveUserIdNotNullConstraintToFranceConnectInformation < ActiveRecord::Migration[6.1]
|
||||
def change
|
||||
change_column_null(:france_connect_informations, :user_id, true)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,8 @@
|
|||
class AddTokenColumnsToFranceConnectInformation < ActiveRecord::Migration[6.1]
|
||||
def change
|
||||
add_column :france_connect_informations, :merge_token, :string
|
||||
add_column :france_connect_informations, :merge_token_created_at, :datetime
|
||||
|
||||
add_index :france_connect_informations, :merge_token
|
||||
end
|
||||
end
|
|
@ -0,0 +1,5 @@
|
|||
class AddForeignKeyToAvisDossierId < ActiveRecord::Migration[6.1]
|
||||
def change
|
||||
add_foreign_key :avis, :dossiers
|
||||
end
|
||||
end
|
|
@ -10,7 +10,7 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(version: 2021_10_06_164955) do
|
||||
ActiveRecord::Schema.define(version: 2021_10_12_100819) do
|
||||
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "plpgsql"
|
||||
|
@ -189,6 +189,7 @@ ActiveRecord::Schema.define(version: 2021_10_06_164955) do
|
|||
t.jsonb "data"
|
||||
t.string "external_id"
|
||||
t.string "fetch_external_data_exceptions", array: true
|
||||
t.jsonb "value_json"
|
||||
t.index ["dossier_id"], name: "index_champs_on_dossier_id"
|
||||
t.index ["parent_id"], name: "index_champs_on_parent_id"
|
||||
t.index ["private"], name: "index_champs_on_private"
|
||||
|
@ -472,11 +473,14 @@ ActiveRecord::Schema.define(version: 2021_10_06_164955) do
|
|||
t.date "birthdate"
|
||||
t.string "birthplace"
|
||||
t.string "france_connect_particulier_id"
|
||||
t.integer "user_id", null: false
|
||||
t.integer "user_id"
|
||||
t.string "email_france_connect"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.jsonb "data"
|
||||
t.string "merge_token"
|
||||
t.datetime "merge_token_created_at"
|
||||
t.index ["merge_token"], name: "index_france_connect_informations_on_merge_token"
|
||||
t.index ["user_id"], name: "index_france_connect_informations_on_user_id"
|
||||
end
|
||||
|
||||
|
@ -813,6 +817,7 @@ ActiveRecord::Schema.define(version: 2021_10_06_164955) do
|
|||
add_foreign_key "assign_tos", "groupe_instructeurs"
|
||||
add_foreign_key "attestation_templates", "procedures"
|
||||
add_foreign_key "attestations", "dossiers"
|
||||
add_foreign_key "avis", "dossiers"
|
||||
add_foreign_key "avis", "experts_procedures"
|
||||
add_foreign_key "bulk_messages_groupe_instructeurs", "bulk_messages"
|
||||
add_foreign_key "bulk_messages_groupe_instructeurs", "groupe_instructeurs"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
describe FranceConnect::ParticulierController, type: :controller do
|
||||
let(:birthdate) { '20150821' }
|
||||
let(:email) { 'test@test.com' }
|
||||
let(:email) { 'EMAIL_from_fc@test.com' }
|
||||
|
||||
let(:user_info) do
|
||||
{
|
||||
|
@ -49,37 +49,68 @@ describe FranceConnect::ParticulierController, type: :controller do
|
|||
.and_return(FranceConnectInformation.new(user_info))
|
||||
end
|
||||
|
||||
context 'when france_connect_particulier_id exist in database' do
|
||||
let!(:france_connect_information) { create(:france_connect_information, :with_user, user_info) }
|
||||
let(:user) { france_connect_information.user }
|
||||
context 'when france_connect_particulier_id exists in database' do
|
||||
let!(:fci) { FranceConnectInformation.create!(user_info.merge(user_id: fc_user&.id)) }
|
||||
|
||||
it { expect { subject }.not_to change { FranceConnectInformation.count } }
|
||||
context 'and is linked to an user' do
|
||||
let(:fc_user) { create(:user, email: 'associated_user@a.com') }
|
||||
|
||||
it do
|
||||
subject
|
||||
expect(user.reload.loged_in_with_france_connect).to eq(User.loged_in_with_france_connects.fetch(:particulier))
|
||||
it { expect { subject }.not_to change { FranceConnectInformation.count } }
|
||||
|
||||
it 'signs in with the fci associated user' do
|
||||
subject
|
||||
expect(controller.current_user).to eq(fc_user)
|
||||
expect(fc_user.reload.loged_in_with_france_connect).to eq(User.loged_in_with_france_connects.fetch(:particulier))
|
||||
end
|
||||
|
||||
context 'and the user has a stored location' do
|
||||
let(:stored_location) { '/plip/plop' }
|
||||
before { controller.store_location_for(:user, stored_location) }
|
||||
|
||||
it { is_expected.to redirect_to(stored_location) }
|
||||
end
|
||||
end
|
||||
|
||||
context 'and the user has a stored location' do
|
||||
let(:stored_location) { '/plip/plop' }
|
||||
before { controller.store_location_for(:user, stored_location) }
|
||||
context 'and is linked an instructeur' do
|
||||
let(:fc_user) { create(:instructeur, email: 'another_email@a.com').user }
|
||||
|
||||
it { is_expected.to redirect_to(stored_location) }
|
||||
end
|
||||
|
||||
context 'and the user is also instructeur' do
|
||||
let!(:instructeur) { create(:instructeur, email: email) }
|
||||
before { subject }
|
||||
|
||||
it { expect(response).to redirect_to(new_user_session_path) }
|
||||
it do
|
||||
expect(response).to redirect_to(new_user_session_path)
|
||||
expect(flash[:alert]).to be_present
|
||||
end
|
||||
end
|
||||
|
||||
it { expect(flash[:alert]).to be_present }
|
||||
context 'and is not linked to an user' do
|
||||
let(:fc_user) { nil }
|
||||
|
||||
context 'and no user with the same email exists' do
|
||||
it 'creates an user with the same email and log in' do
|
||||
expect { subject }.to change { User.count }.by(1)
|
||||
|
||||
user = User.last
|
||||
|
||||
expect(user.email).to eq(email.downcase)
|
||||
expect(controller.current_user).to eq(user)
|
||||
expect(response).to redirect_to(root_path)
|
||||
end
|
||||
end
|
||||
|
||||
context 'and an user with the same email exists' do
|
||||
let!(:preexisting_user) { create(:user, email: email) }
|
||||
|
||||
it 'redirects to the merge process' do
|
||||
expect { subject }.not_to change { User.count }
|
||||
|
||||
expect(response).to redirect_to(france_connect_particulier_merge_path(fci.reload.merge_token))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when france_connect_particulier_id does not exist in database' do
|
||||
it { expect { subject }.to change { FranceConnectInformation.count }.by(1) }
|
||||
it { expect { subject }.to change { FranceConnectInformation.count }.by(1) }
|
||||
|
||||
describe 'FranceConnectInformation attributs' do
|
||||
let(:stored_fci) { FranceConnectInformation.last }
|
||||
|
@ -104,4 +135,138 @@ describe FranceConnect::ParticulierController, type: :controller do
|
|||
it { expect(flash[:alert]).to be_present }
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.shared_examples "a method that needs a valid merge token" do
|
||||
context 'when the merge token is invalid' do
|
||||
before do
|
||||
merge_token
|
||||
fci.update(merge_token_created_at: 2.years.ago)
|
||||
end
|
||||
|
||||
it do
|
||||
if format == :js
|
||||
subject
|
||||
expect(response.body).to eq("window.location.href='/'")
|
||||
else
|
||||
expect(subject).to redirect_to root_path
|
||||
end
|
||||
expect(flash.alert).to eq('Votre compte FranceConnect a expiré, veuillez recommencer.')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#merge' do
|
||||
let(:fci) { FranceConnectInformation.create!(user_info) }
|
||||
let(:merge_token) { fci.create_merge_token! }
|
||||
let(:format) { :html }
|
||||
|
||||
subject { get :merge, params: { merge_token: merge_token } }
|
||||
|
||||
context 'when the merge token is valid' do
|
||||
it { expect(subject).to have_http_status(:ok) }
|
||||
end
|
||||
|
||||
it_behaves_like "a method that needs a valid merge token"
|
||||
|
||||
context 'when the merge token does not exist' do
|
||||
let(:merge_token) { 'i do not exist' }
|
||||
|
||||
it do
|
||||
expect(subject).to redirect_to root_path
|
||||
expect(flash.alert).to eq('Votre compte FranceConnect a expiré, veuillez recommencer.')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#merge_with_existing_account' do
|
||||
let(:fci) { FranceConnectInformation.create!(user_info) }
|
||||
let(:merge_token) { fci.create_merge_token! }
|
||||
let(:email) { 'EXISTING_account@a.com ' }
|
||||
let(:password) { 'my-s3cure-p4ssword' }
|
||||
let(:format) { :js }
|
||||
|
||||
subject { post :merge_with_existing_account, params: { merge_token: merge_token, email: email, password: password }, format: format }
|
||||
|
||||
it_behaves_like "a method that needs a valid merge token"
|
||||
|
||||
context 'when the credentials are ok' do
|
||||
let!(:user) { create(:user, email: email, password: password) }
|
||||
|
||||
it 'merges the account, signs in, and delete the merge token' do
|
||||
subject
|
||||
fci.reload
|
||||
|
||||
expect(fci.user).to eq(user)
|
||||
expect(fci.merge_token).to be_nil
|
||||
expect(controller.current_user).to eq(user)
|
||||
end
|
||||
|
||||
context 'but the targeted user is an instructeur' do
|
||||
let!(:user) { create(:instructeur, email: email, password: password).user }
|
||||
|
||||
it 'redirects to the root page' do
|
||||
subject
|
||||
fci.reload
|
||||
|
||||
expect(fci.user).to be_nil
|
||||
expect(fci.merge_token).not_to be_nil
|
||||
expect(controller.current_user).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the credentials are not ok' do
|
||||
let!(:user) { create(:user, email: email, password: 'another password #$21$%%') }
|
||||
|
||||
it 'increases the failed attempts counter' do
|
||||
subject
|
||||
fci.reload
|
||||
|
||||
expect(fci.user).to be_nil
|
||||
expect(fci.merge_token).not_to be_nil
|
||||
expect(controller.current_user).to be_nil
|
||||
expect(user.reload.failed_attempts).to eq(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#merge_with_new_account' do
|
||||
let(:fci) { FranceConnectInformation.create!(user_info) }
|
||||
let(:merge_token) { fci.create_merge_token! }
|
||||
let(:email) { ' Account@a.com ' }
|
||||
let(:format) { :js }
|
||||
|
||||
subject { post :merge_with_new_account, params: { merge_token: merge_token, email: email }, format: format }
|
||||
|
||||
it_behaves_like "a method that needs a valid merge token"
|
||||
|
||||
context 'when the email does not belong to any user' do
|
||||
it 'creates the account, signs in, and delete the merge token' do
|
||||
subject
|
||||
fci.reload
|
||||
|
||||
expect(fci.user.email).to eq(email.downcase.strip)
|
||||
expect(fci.merge_token).to be_nil
|
||||
expect(controller.current_user).to eq(fci.user)
|
||||
expect(response.body).to include("window.location.href='/'")
|
||||
end
|
||||
end
|
||||
|
||||
context 'when an account with the same email exists' do
|
||||
let!(:user) { create(:user, email: email) }
|
||||
|
||||
render_views
|
||||
|
||||
it 'asks for the corresponding password' do
|
||||
subject
|
||||
fci.reload
|
||||
|
||||
expect(fci.user).to be_nil
|
||||
expect(fci.merge_token).not_to be_nil
|
||||
expect(controller.current_user).to be_nil
|
||||
|
||||
expect(response.body).to include('entrez votre mot de passe')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -185,6 +185,10 @@ FactoryBot.define do
|
|||
type_de_champ { association :type_de_champ_annuaire_education, procedure: dossier.procedure }
|
||||
end
|
||||
|
||||
factory :champ_cnaf, class: 'Champs::CnafChamp' do
|
||||
type_de_champ { association :type_de_champ_cnaf, procedure: dossier.procedure }
|
||||
end
|
||||
|
||||
factory :champ_siret, class: 'Champs::SiretChamp' do
|
||||
association :type_de_champ, factory: [:type_de_champ_siret]
|
||||
association :etablissement, factory: [:etablissement]
|
||||
|
|
|
@ -205,6 +205,12 @@ FactoryBot.define do
|
|||
end
|
||||
end
|
||||
|
||||
trait :with_cnaf do
|
||||
after(:build) do |procedure, _evaluator|
|
||||
build(:type_de_champ_cnaf, procedure: procedure)
|
||||
end
|
||||
end
|
||||
|
||||
trait :with_explication do
|
||||
after(:build) do |procedure, _evaluator|
|
||||
build(:type_de_champ_explication, procedure: procedure)
|
||||
|
|
|
@ -151,6 +151,9 @@ FactoryBot.define do
|
|||
factory :type_de_champ_annuaire_education do
|
||||
type_champ { TypeDeChamp.type_champs.fetch(:annuaire_education) }
|
||||
end
|
||||
factory :type_de_champ_cnaf do
|
||||
type_champ { TypeDeChamp.type_champs.fetch(:cnaf) }
|
||||
end
|
||||
factory :type_de_champ_carte do
|
||||
type_champ { TypeDeChamp.type_champs.fetch(:carte) }
|
||||
end
|
||||
|
|
167
spec/features/api_particulier/api_particulier_spec.rb
Normal file
167
spec/features/api_particulier/api_particulier_spec.rb
Normal file
|
@ -0,0 +1,167 @@
|
|||
feature 'fetch API Particulier Data', js: true do
|
||||
let(:administrateur) { create(:administrateur) }
|
||||
|
||||
let(:expected_token) { 'd7e9c9f4c3ca00caadde31f50fd4521a' }
|
||||
|
||||
let(:expected_sources) do
|
||||
{
|
||||
"cnaf" =>
|
||||
{
|
||||
"adresse" => ["identite", "complementIdentite", "complementIdentiteGeo", "numeroRue", "lieuDit", "codePostalVille", "pays"],
|
||||
"allocataires" => ["nomPrenom", "dateDeNaissance", "sexe"],
|
||||
"enfants" => ["nomPrenom", "dateDeNaissance", "sexe"],
|
||||
"quotient_familial" => ["quotientFamilial", "annee", "mois"]
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
before do
|
||||
stub_const("API_PARTICULIER_URL", "https://particulier.api.gouv.fr/api")
|
||||
Flipper.enable(:api_particulier)
|
||||
end
|
||||
|
||||
context "when an administrateur is logged" do
|
||||
let(:procedure) do
|
||||
create(:procedure, :with_service, :with_instructeur,
|
||||
aasm_state: :brouillon,
|
||||
administrateurs: [administrateur],
|
||||
libelle: "libellé de la procédure",
|
||||
path: "libelle-de-la-procedure")
|
||||
end
|
||||
|
||||
before { login_as administrateur.user, scope: :user }
|
||||
|
||||
scenario 'it can enable api particulier' do
|
||||
visit admin_procedure_path(procedure)
|
||||
expect(page).to have_content("Configurer le jeton API particulier")
|
||||
|
||||
find('#api-particulier').click
|
||||
expect(page).to have_current_path(admin_procedure_api_particulier_path(procedure))
|
||||
|
||||
find('#add-jeton').click
|
||||
expect(page).to have_current_path(admin_procedure_api_particulier_jeton_path(procedure))
|
||||
|
||||
fill_in 'procedure_api_particulier_token', with: expected_token
|
||||
VCR.use_cassette("api_particulier/success/introspect") { click_on 'Enregistrer' }
|
||||
expect(page).to have_text('Le jeton a bien été mis à jour')
|
||||
expect(page).to have_current_path(admin_procedure_api_particulier_sources_path(procedure))
|
||||
|
||||
['allocataires', 'enfants'].each do |scope|
|
||||
within("##{scope}") do
|
||||
check('noms et prénoms')
|
||||
check('date de naissance')
|
||||
check('sexe')
|
||||
end
|
||||
end
|
||||
|
||||
within("#adresse") do
|
||||
check('identité')
|
||||
check('complément d’identité')
|
||||
check('complément d’identité géographique')
|
||||
check('numéro et rue')
|
||||
check('lieu-dit')
|
||||
check('code postal et ville')
|
||||
check('pays')
|
||||
end
|
||||
|
||||
within("#quotient_familial") do
|
||||
check('quotient familial')
|
||||
check('année')
|
||||
check('mois')
|
||||
end
|
||||
|
||||
click_on "Enregistrer"
|
||||
|
||||
within("#enfants") do
|
||||
expect(find('input[value=nomPrenom]')).to be_checked
|
||||
end
|
||||
|
||||
expect(procedure.reload.api_particulier_sources).to eq(expected_sources)
|
||||
|
||||
visit champs_admin_procedure_path(procedure)
|
||||
|
||||
add_champ
|
||||
select('Données de la Caisse nationale des allocations familiales', from: 'champ-0-type_champ')
|
||||
fill_in 'champ-0-libelle', with: 'libellé de champ'
|
||||
blur
|
||||
expect(page).to have_content('Formulaire enregistré')
|
||||
|
||||
visit admin_procedure_path(procedure)
|
||||
find('#publish-procedure-link').click
|
||||
expect(find_field('procedure_path').value).to eq procedure.path
|
||||
fill_in 'lien_site_web', with: 'http://some.website'
|
||||
click_on 'Publier'
|
||||
|
||||
expect(page).to have_text('Démarche publiée')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when an user is logged' do
|
||||
let(:user) { create(:user) }
|
||||
let(:api_particulier_token) { '29eb50b65f64e8e00c0847a8bbcbd150e1f847' }
|
||||
let(:numero_allocataire) { '5843972' }
|
||||
let(:code_postal) { '92110' }
|
||||
let(:instructeur) { create(:instructeur) }
|
||||
|
||||
let(:procedure) do
|
||||
create(:procedure, :for_individual, :with_service, :with_cnaf, :published,
|
||||
libelle: "libellé de la procédure",
|
||||
path: "libelle-de-la-procedure",
|
||||
instructeurs: [instructeur],
|
||||
api_particulier_sources: expected_sources,
|
||||
api_particulier_token: api_particulier_token)
|
||||
end
|
||||
|
||||
before { login_as user, scope: :user }
|
||||
|
||||
scenario 'it can fill an cnaf champ' do
|
||||
visit commencer_path(path: procedure.path)
|
||||
click_on 'Commencer la démarche'
|
||||
|
||||
choose 'Monsieur'
|
||||
fill_in 'individual_nom', with: 'Nom'
|
||||
fill_in 'individual_prenom', with: 'Prenom'
|
||||
|
||||
click_button('Continuer')
|
||||
|
||||
fill_in 'Le numéro d’allocataire CAF', with: numero_allocataire
|
||||
fill_in 'Le code postal', with: 'wrong_code'
|
||||
|
||||
blur
|
||||
expect(page).to have_css('span', text: 'Brouillon enregistré', visible: true)
|
||||
|
||||
dossier = Dossier.last
|
||||
expect(dossier.champs.first.code_postal).to eq('wrong_code')
|
||||
|
||||
click_on 'Déposer le dossier'
|
||||
expect(page).to have_content(/code postal doit posséder 5 caractères/)
|
||||
|
||||
fill_in 'Le code postal', with: code_postal
|
||||
|
||||
VCR.use_cassette("api_particulier/success/composition_familiale") do
|
||||
perform_enqueued_jobs { click_on 'Déposer le dossier' }
|
||||
end
|
||||
|
||||
visit demande_dossier_path(dossier)
|
||||
expect(page).to have_content(/Des données.*ont été reçues depuis la CAF/)
|
||||
|
||||
log_out
|
||||
|
||||
login_as instructeur.user, scope: :user
|
||||
|
||||
visit instructeur_dossier_path(procedure, dossier)
|
||||
|
||||
expect(page).to have_content('code postal et ville 92110 Clichy')
|
||||
expect(page).to have_content('identité Mr SNOW Eric')
|
||||
expect(page).to have_content('complément d’identité ne connait rien')
|
||||
expect(page).to have_content('numéro et rue 109 rue La Boétie')
|
||||
expect(page).to have_content('pays FRANCE')
|
||||
expect(page).to have_content('complément d’identité géographique au nord de paris')
|
||||
expect(page).to have_content('lieu-dit glagla')
|
||||
expect(page).to have_content('ERIC SNOW masculin 07/01/1991')
|
||||
expect(page).to have_content('SANSA SNOW féminin 15/01/1992')
|
||||
expect(page).to have_content('PAUL SNOW masculin 04/01/2018')
|
||||
expect(page).to have_content('1856 6 2021')
|
||||
end
|
||||
end
|
||||
end
|
|
@ -21,62 +21,85 @@ feature 'France Connect Particulier Connexion' do
|
|||
end
|
||||
|
||||
context 'when user is on login page' do
|
||||
before do
|
||||
visit new_user_session_path
|
||||
end
|
||||
before { visit new_user_session_path }
|
||||
|
||||
scenario 'link to France Connect is present' do
|
||||
expect(page).to have_css('.france-connect-login-button')
|
||||
end
|
||||
|
||||
context 'and click on france connect link' do
|
||||
let(:code) { 'plop' }
|
||||
|
||||
context 'when authentification is ok' do
|
||||
before do
|
||||
allow_any_instance_of(FranceConnectParticulierClient).to receive(:authorization_uri).and_return(france_connect_particulier_callback_path(code: code))
|
||||
allow(FranceConnectService).to receive(:retrieve_user_informations_particulier).and_return(france_connect_information)
|
||||
end
|
||||
|
||||
context 'when is the first connexion' do
|
||||
let(:france_connect_information) do
|
||||
build(:france_connect_information,
|
||||
france_connect_particulier_id: france_connect_particulier_id,
|
||||
given_name: given_name,
|
||||
family_name: family_name,
|
||||
birthdate: birthdate,
|
||||
birthplace: birthplace,
|
||||
gender: gender,
|
||||
email_france_connect: email)
|
||||
context 'when no user is linked' do
|
||||
let(:france_connect_information) { build(:france_connect_information, user_info) }
|
||||
|
||||
context 'and no user has the same email' do
|
||||
before { page.find('.france-connect-login-button').click }
|
||||
|
||||
scenario 'he is redirected to user dossiers page' do
|
||||
expect(page).to have_content('Dossiers')
|
||||
expect(User.find_by(email: email)).not_to be nil
|
||||
end
|
||||
end
|
||||
|
||||
before do
|
||||
page.find('.france-connect-login-button').click
|
||||
end
|
||||
context 'and an user exists with the same email' do
|
||||
let!(:user) { create(:user, email: email, password: 'my-s3cure-p4ssword') }
|
||||
|
||||
scenario 'he is redirected to user dossiers page' do
|
||||
expect(page).to have_content('Dossiers')
|
||||
before do
|
||||
page.find('.france-connect-login-button').click
|
||||
end
|
||||
|
||||
scenario 'he is redirected to the merge page' do
|
||||
expect(page).to have_content('Fusion des comptes')
|
||||
end
|
||||
|
||||
scenario 'it merges its account' do
|
||||
page.find('#it-is-mine').click
|
||||
fill_in 'password', with: 'my-s3cure-p4ssword'
|
||||
click_on 'Fusionner les comptes'
|
||||
|
||||
expect(page).to have_content('Dossiers')
|
||||
end
|
||||
|
||||
scenario 'it uses another email that belongs to nobody' do
|
||||
page.find('#it-is-not-mine').click
|
||||
fill_in 'email', with: 'new_email@a.com'
|
||||
click_on 'Utiliser ce mail'
|
||||
|
||||
expect(page).to have_content('Dossiers')
|
||||
end
|
||||
|
||||
context 'and the user wants an email that belongs to another account', js: true do
|
||||
let!(:another_user) { create(:user, email: 'an_existing_email@a.com', password: 'my-s3cure-p4ssword') }
|
||||
|
||||
scenario 'it uses another email that belongs to another account' do
|
||||
page.find('#it-is-not-mine').click
|
||||
fill_in 'email', with: 'an_existing_email@a.com'
|
||||
click_on 'Utiliser ce mail'
|
||||
|
||||
expect(page).to have_css('#password-for-another-account', visible: true)
|
||||
|
||||
within '.new-account-password-confirmation' do
|
||||
fill_in 'password', with: 'my-s3cure-p4ssword'
|
||||
click_on 'Fusionner les comptes'
|
||||
end
|
||||
|
||||
expect(page).to have_content('Dossiers')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when is not the first connexion' do
|
||||
context 'when a user is linked' do
|
||||
let!(:france_connect_information) do
|
||||
create(:france_connect_information,
|
||||
:with_user,
|
||||
france_connect_particulier_id: france_connect_particulier_id,
|
||||
given_name: given_name,
|
||||
family_name: family_name,
|
||||
birthdate: birthdate,
|
||||
birthplace: birthplace,
|
||||
gender: gender,
|
||||
email_france_connect: email,
|
||||
created_at: Time.zone.parse('12/12/2012'),
|
||||
updated_at: Time.zone.parse('12/12/2012'))
|
||||
create(:france_connect_information, :with_user, user_info.merge(created_at: Time.zone.parse('12/12/2012'), updated_at: Time.zone.parse('12/12/2012')))
|
||||
end
|
||||
|
||||
before do
|
||||
page.find('.france-connect-login-button').click
|
||||
end
|
||||
before { page.find('.france-connect-login-button').click }
|
||||
|
||||
scenario 'he is redirected to user dossiers page' do
|
||||
expect(page).to have_content('Dossiers')
|
||||
|
|
|
@ -63,7 +63,7 @@ feature 'The routing', js: true do
|
|||
|
||||
# publish
|
||||
publish_procedure(procedure)
|
||||
log_out(old_layout: true)
|
||||
log_out
|
||||
|
||||
# 2 users fill a dossier in each group
|
||||
user_send_dossier(scientifique_user, 'scientifique')
|
||||
|
@ -222,15 +222,4 @@ feature 'The routing', js: true do
|
|||
|
||||
expect(page).to have_text('Mot de passe enregistré')
|
||||
end
|
||||
|
||||
def log_out(old_layout: false)
|
||||
if old_layout
|
||||
page.all('.dropdown-button').first.click
|
||||
click_on 'Se déconnecter'
|
||||
else
|
||||
click_button(title: 'Mon compte')
|
||||
click_on 'Se déconnecter'
|
||||
end
|
||||
expect(page).to have_current_path(root_path)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
http_interactions:
|
||||
- request:
|
||||
method: get
|
||||
uri: https://particulier-test.api.gouv.fr/api/v2/composition-familiale?codePostal=92110&numeroAllocataire=5843972
|
||||
uri: https://particulier.api.gouv.fr/api/v2/composition-familiale?codePostal=92110&numeroAllocataire=5843972
|
||||
body:
|
||||
encoding: US-ASCII
|
||||
string: ''
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
http_interactions:
|
||||
- request:
|
||||
method: get
|
||||
uri: https://particulier-test.api.gouv.fr/api/v2/composition-familiale?codePostal=92110&numeroAllocataire=5843972
|
||||
uri: https://particulier.api.gouv.fr/api/v2/composition-familiale?codePostal=92110&numeroAllocataire=5843972
|
||||
body:
|
||||
encoding: US-ASCII
|
||||
string: ''
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
describe APIParticulier::CNAFAdapter do
|
||||
describe APIParticulier::CnafAdapter do
|
||||
let(:adapter) { described_class.new(api_particulier_token, numero_allocataire, code_postal, requested_sources) }
|
||||
|
||||
before { stub_const("API_PARTICULIER_URL", "https://particulier-test.api.gouv.fr/api") }
|
||||
before { stub_const("API_PARTICULIER_URL", "https://particulier.api.gouv.fr/api") }
|
||||
|
||||
describe '#to_params' do
|
||||
let(:api_particulier_token) { '29eb50b65f64e8e00c0847a8bbcbd150e1f847' }
|
||||
|
@ -63,7 +63,7 @@ describe APIParticulier::CNAFAdapter do
|
|||
context 'when no sources is requested' do
|
||||
let(:requested_sources) { {} }
|
||||
|
||||
it { expect { subject }.to raise_error(APIParticulier::CNAFAdapter::InvalidSchemaError) }
|
||||
it { expect { subject }.to raise_error(APIParticulier::CnafAdapter::InvalidSchemaError) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -31,6 +31,12 @@ describe APIParticulier::Services::SourcesService do
|
|||
|
||||
it { is_expected.to match(cnaf_allocataires_and_enfants) }
|
||||
end
|
||||
|
||||
context 'when a procedure has an unknown scope' do
|
||||
let(:api_particulier_scopes) { ['unknown_scope'] }
|
||||
|
||||
it { is_expected.to match({}) }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#sanitize' do
|
||||
|
|
103
spec/models/champs/cnaf_champ_spec.rb
Normal file
103
spec/models/champs/cnaf_champ_spec.rb
Normal file
|
@ -0,0 +1,103 @@
|
|||
describe Champs::CnafChamp, type: :model do
|
||||
let(:champ) { described_class.new }
|
||||
|
||||
describe 'numero_allocataire and code_postal' do
|
||||
before do
|
||||
champ.numero_allocataire = '1234567'
|
||||
champ.code_postal = '12345'
|
||||
end
|
||||
|
||||
it 'saves numero_allocataire and code_postal' do
|
||||
expect(champ.numero_allocataire).to eq('1234567')
|
||||
expect(champ.code_postal).to eq('12345')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'external_id' do
|
||||
context 'when only one data is given' do
|
||||
before do
|
||||
champ.numero_allocataire = '1234567'
|
||||
champ.save
|
||||
end
|
||||
|
||||
it { expect(champ.external_id).to be_nil }
|
||||
end
|
||||
|
||||
context 'when all data required for an external fetch are given' do
|
||||
before do
|
||||
champ.numero_allocataire = '1234567'
|
||||
champ.code_postal = '12345'
|
||||
champ.save
|
||||
end
|
||||
|
||||
it { expect(JSON.parse(champ.external_id)).to eq({ "code_postal" => "12345", "numero_allocataire" => "1234567" }) }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#validate' do
|
||||
let(:numero_allocataire) { '1234567' }
|
||||
let(:code_postal) { '12345' }
|
||||
let(:champ) { described_class.new(dossier: create(:dossier), type_de_champ: create(:type_de_champ_cnaf)) }
|
||||
let(:validation_context) { :create }
|
||||
|
||||
subject { champ.valid?(validation_context) }
|
||||
|
||||
before do
|
||||
champ.numero_allocataire = numero_allocataire
|
||||
champ.code_postal = code_postal
|
||||
end
|
||||
|
||||
context 'when numero_allocataire and code_postal are valids' do
|
||||
it { is_expected.to be true }
|
||||
end
|
||||
|
||||
context 'when numero_allocataire and code_postal are nil' do
|
||||
let(:numero_allocataire) { nil }
|
||||
let(:code_postal) { nil }
|
||||
|
||||
it { is_expected.to be true }
|
||||
end
|
||||
|
||||
context 'when only code_postal is nil' do
|
||||
let(:code_postal) { nil }
|
||||
|
||||
it do
|
||||
is_expected.to be false
|
||||
expect(champ.errors.full_messages).to eq(["Code postal doit posséder 5 caractères"])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when only numero_allocataire is nil' do
|
||||
let(:numero_allocataire) { nil }
|
||||
|
||||
it do
|
||||
is_expected.to be false
|
||||
expect(champ.errors.full_messages).to eq(["Numero allocataire doit être composé au maximum de 7 chiffres"])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when numero_allocataire is invalid' do
|
||||
let(:numero_allocataire) { '123456a' }
|
||||
|
||||
it do
|
||||
is_expected.to be false
|
||||
expect(champ.errors.full_messages).to eq(["Numero allocataire doit être composé au maximum de 7 chiffres"])
|
||||
end
|
||||
|
||||
context 'and the validation_context is :brouillon' do
|
||||
let(:validation_context) { :brouillon }
|
||||
|
||||
it { is_expected.to be true }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when code_postal is invalid' do
|
||||
let(:code_postal) { '123456' }
|
||||
|
||||
it do
|
||||
is_expected.to be false
|
||||
expect(champ.errors.full_messages).to eq(["Code postal doit posséder 5 caractères"])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -19,6 +19,12 @@ RSpec.describe Expert, type: :model do
|
|||
|
||||
subject { new_expert.merge(old_expert) }
|
||||
|
||||
context 'when the old expert does not exist' do
|
||||
let(:old_expert) { nil }
|
||||
|
||||
it { expect { subject }.not_to raise_error }
|
||||
end
|
||||
|
||||
context 'when an old expert access a procedure' do
|
||||
let(:procedure) { create(:procedure) }
|
||||
|
||||
|
@ -28,7 +34,7 @@ RSpec.describe Expert, type: :model do
|
|||
end
|
||||
|
||||
it 'transfers the access to the new expert' do
|
||||
expect(procedure.reload.experts). to match_array(new_expert)
|
||||
expect(procedure.reload.experts).to match_array(new_expert)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -9,24 +9,52 @@ describe FranceConnectInformation, type: :model do
|
|||
|
||||
describe 'associate_user!' do
|
||||
context 'when there is no user with same email' do
|
||||
let(:email) { 'A@email.com' }
|
||||
let(:fci) { build(:france_connect_information) }
|
||||
let(:subject) { fci.associate_user! }
|
||||
|
||||
subject { fci.associate_user!(email) }
|
||||
|
||||
it { expect { subject }.to change(User, :count).by(1) }
|
||||
|
||||
it do
|
||||
subject
|
||||
expect(fci.user.email).to eq(fci.email_france_connect)
|
||||
expect(fci.user.email).to eq('a@email.com')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#valid_for_merge?' do
|
||||
let(:fci) { create(:france_connect_information) }
|
||||
|
||||
subject { fci.valid_for_merge? }
|
||||
|
||||
context 'when the merge token is young enough' do
|
||||
before { fci.merge_token_created_at = 1.minute.ago }
|
||||
|
||||
it { is_expected.to be(true) }
|
||||
|
||||
context 'but the fci is already linked to an user' do
|
||||
before { fci.update(user: create(:user)) }
|
||||
|
||||
it { is_expected.to be(false) }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a user with same email (but who is not an instructeur) exist' do
|
||||
let(:user) { create(:user) }
|
||||
let(:fci) { build(:france_connect_information, email_france_connect: user.email) }
|
||||
let(:subject) { fci.associate_user! }
|
||||
context 'when the merge token is too old' do
|
||||
before { fci.merge_token_created_at = (FranceConnectInformation::MERGE_VALIDITY + 1.minute).ago }
|
||||
|
||||
before { subject }
|
||||
it { is_expected.to be(false) }
|
||||
end
|
||||
end
|
||||
|
||||
it { expect(fci.user).to eq(user) }
|
||||
describe '#create_merge_token!' do
|
||||
let(:fci) { create(:france_connect_information) }
|
||||
|
||||
it 'returns a merge_token and register it s creation date' do
|
||||
token = fci.create_merge_token!
|
||||
|
||||
expect(fci.merge_token).to eq(token)
|
||||
expect(fci.merge_token_created_at).not_to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue