feat(cojo): add cojo type de champ

This commit is contained in:
Paul Chavard 2023-05-30 14:42:36 +02:00
parent c74351459e
commit d5820277c0
31 changed files with 361 additions and 12 deletions

View file

@ -48,6 +48,8 @@
= render partial: "shared/champs/rna/show", locals: { champ: champ, profile: @profile }
- when TypeDeChamp.type_champs.fetch(:epci)
= render partial: "shared/champs/epci/show", locals: { champ: champ }
- when TypeDeChamp.type_champs.fetch(:cojo)
= render partial: "shared/champs/cojo/show", locals: { champ: champ, profile: @profile }
- when TypeDeChamp.type_champs.fetch(:date)
%p= champ.to_s
- when TypeDeChamp.type_champs.fetch(:datetime)

View file

@ -0,0 +1,9 @@
class EditableChamp::COJOComponent < EditableChamp::EditableChampBaseComponent
def input_group_class
if @champ.accreditation_success?
'fr-input-group--valid'
elsif @champ.accreditation_error?
'fr-input-group--error'
end
end
end

View file

@ -0,0 +1,7 @@
---
en:
accreditation_number_label: Accreditation number
accreditation_number_notice: Identification number issued by Paris 2024
accreditation_birthdate_label: Date of birth
accreditation_number_error: Invalid accreditation number
accreditation_number_verification_pending: Accreditation number verification in progress

View file

@ -0,0 +1,7 @@
---
fr:
accreditation_number_label: Numéro daccréditation
accreditation_number_notice: Numéro didentification délivré par Paris 2024
accreditation_birthdate_label: Date de naissance
accreditation_number_error: Le numéro daccréditation est incorrect
accreditation_number_verification_pending: Vérification du numéro daccréditation en cours

View file

@ -0,0 +1,23 @@
.fr-input-group{ class: input_group_class }
= @form.label :accreditation_number, for: @champ.accreditation_number_input_id, class: 'fr-label' do
- safe_join [t('.accreditation_number_label'), @champ.required? ? render(EditableChamp::AsteriskMandatoryComponent.new) : ''], ' '
%p.fr-hint-text{ id: dom_id(@champ, :accreditation_number_notice) }= t('.accreditation_number_notice')
= @form.text_field :accreditation_number,
required: @champ.required?,
aria: { describedby: [dom_id(@champ, :accreditation_number_notice), @champ.accreditation_error? ? dom_id(@champ, :accreditation_number_error) : nil].compact.join(' ') },
data: { controller: 'format', format: 'integer' },
class: "width-33-desktop fr-input small-margin", id: @champ.accreditation_number_input_id
- if @champ.accreditation_error?
%p.fr-error-text{ id: dom_id(@champ, :accreditation_number_error) }= t('.accreditation_number_error')
- elsif @champ.fetch_external_data_pending?
%p.fr-info-text= t('.accreditation_number_verification_pending')
.fr-input-group{ class: input_group_class }
= @form.label :accreditation_birthdate, for: @champ.accreditation_birthdate_input_id, class: 'fr-label' do
- safe_join [t('.accreditation_birthdate_label'), @champ.required? ? render(EditableChamp::AsteriskMandatoryComponent.new) : ''], ' '
= @form.date_field :accreditation_birthdate,
required: @champ.required?,
aria: { describedby: dom_id(@champ, :accreditation_birthdate) },
class: "width-33-desktop fr-input small-margin", id: @champ.accreditation_birthdate_input_id

View file

@ -471,8 +471,23 @@ module Users
def champs_public_params
champs_params = params.require(:dossier).permit(champs_public_attributes: [
:id, :value, :value_other, :external_id, :primary_value, :secondary_value, :numero_allocataire, :code_postal, :identifiant, :numero_fiscal, :reference_avis, :ine, :piece_justificative_file, :code_departement, value: [],
champs_attributes: [:id, :_destroy, :value, :value_other, :external_id, :primary_value, :secondary_value, :numero_allocataire, :code_postal, :identifiant, :numero_fiscal, :reference_avis, :ine, :piece_justificative_file, :departement, :code_departement, value: []]
:id,
:value,
:value_other,
:external_id,
:primary_value,
:secondary_value,
:numero_allocataire,
:code_postal,
:identifiant,
:numero_fiscal,
:reference_avis,
:ine,
:piece_justificative_file,
:code_departement,
:accreditation_number,
:accreditation_birthdate,
value: []
])
champs_params[:champs_public_all_attributes] = champs_params.delete(:champs_public_attributes) || {}
champs_params

View file

@ -84,6 +84,7 @@ class API::V2::Schema < GraphQL::Schema
Types::Champs::Descriptor::CheckboxChampDescriptorType,
Types::Champs::Descriptor::CiviliteChampDescriptorType,
Types::Champs::Descriptor::CnafChampDescriptorType,
Types::Champs::Descriptor::COJOChampDescriptorType,
Types::Champs::Descriptor::CommuneChampDescriptorType,
Types::Champs::Descriptor::DateChampDescriptorType,
Types::Champs::Descriptor::DatetimeChampDescriptorType,

View file

@ -190,6 +190,34 @@ exceed the size of a 32-bit integer, it's encoded as a string.
"""
scalar BigInt
type COJOChampDescriptor implements ChampDescriptor {
"""
Description des champs dun bloc répétable.
"""
champDescriptors: [ChampDescriptor!] @deprecated(reason: "Utilisez le champ `RepetitionChampDescriptor.champ_descriptors` à la place.")
"""
Description du champ.
"""
description: String
id: ID!
"""
Libellé du champ.
"""
label: String!
"""
Est-ce que le champ est obligatoire ?
"""
required: Boolean!
"""
Type de la valeur du champ.
"""
type: TypeDeChamp! @deprecated(reason: "Utilisez le champ `__typename` à la place.")
}
type CarteChamp implements Champ {
geoAreas: [GeoArea!]!
id: ID!
@ -3699,6 +3727,11 @@ enum TypeDeChamp {
"""
cnaf
"""
Accréditation Paris 2024
"""
cojo
"""
Communes
"""

View file

@ -94,6 +94,8 @@ module Types
Types::Champs::Descriptor::MesriChampDescriptorType
when TypeDeChamp.type_champs.fetch(:epci)
Types::Champs::Descriptor::EpciChampDescriptorType
when TypeDeChamp.type_champs.fetch(:cojo)
Types::Champs::Descriptor::COJOChampDescriptorType
end
end
end

View file

@ -0,0 +1,5 @@
module Types::Champs::Descriptor
class COJOChampDescriptorType < Types::BaseObject
implements Types::ChampDescriptorType
end
end

View file

@ -15,6 +15,12 @@ export class FormatController extends ApplicationController {
const target = event.target as HTMLInputElement;
target.value = this.formatIBAN(target.value);
});
break;
case 'integer':
this.on('input', (event) => {
const target = event.target as HTMLInputElement;
target.value = this.formatInteger(target.value);
});
}
}
@ -28,4 +34,8 @@ export class FormatController extends ApplicationController {
.replace(/(.{4})/g, '$1 ')
.trim();
}
private formatInteger(value: string) {
return value.replace(/[^\d]/g, '');
}
}

View file

@ -0,0 +1,64 @@
class Champs::COJOChamp < Champ
store_accessor :value_json, :accreditation_number, :accreditation_birthdate
store_accessor :data, :accreditation_success, :accreditation_first_name, :accreditation_last_name
after_validation :update_external_id
def accreditation_birthdate
Date.parse(super)
rescue ArgumentError, TypeError
nil
end
def accreditation_success?
accreditation_success == true
end
def accreditation_error?
accreditation_success == false
end
def blank?
accreditation_success.nil?
end
def fetch_external_data?
true
end
def poll_external_data?
true
end
def fetch_external_data
COJOService.new.(accreditation_number:, accreditation_birthdate:)
end
def to_s
"#{accreditation_number} #{accreditation_birthdate}"
end
def accreditation_number_input_id
"#{input_id}-accreditation_number"
end
def accreditation_birthdate_input_id
"#{input_id}-accreditation_birthdate"
end
def focusable_input_id
accreditation_number_input_id
end
private
def update_external_id
if accreditation_number_changed? || accreditation_birthdate_changed?
if accreditation_number.present? && accreditation_birthdate.present? && /\A\d+\z/.match?(accreditation_number)
self.external_id = { accreditation_number:, accreditation_birthdate: }.to_json
else
self.external_id = nil
end
end
end
end

View file

@ -18,7 +18,9 @@ class TypeDeChamp < ApplicationRecord
self.ignored_columns += [:migrated_parent, :revision_id, :parent_id, :order_place]
FILE_MAX_SIZE = 200.megabytes
FEATURE_FLAGS = {}
FEATURE_FLAGS = {
cojo: :cojo_type_de_champ
}
MINIMUM_TEXTAREA_CHARACTER_LIMIT_LENGTH = 400
STRUCTURE = :structure
@ -68,7 +70,8 @@ class TypeDeChamp < ApplicationRecord
cnaf: REFERENTIEL_EXTERNE,
dgfip: REFERENTIEL_EXTERNE,
pole_emploi: REFERENTIEL_EXTERNE,
mesri: REFERENTIEL_EXTERNE
mesri: REFERENTIEL_EXTERNE,
cojo: REFERENTIEL_EXTERNE
}
enum type_champs: {
@ -107,7 +110,8 @@ class TypeDeChamp < ApplicationRecord
dgfip: 'dgfip',
pole_emploi: 'pole_emploi',
mesri: 'mesri',
epci: 'epci'
epci: 'epci',
cojo: 'cojo'
}
store_accessor :options,
@ -569,7 +573,8 @@ class TypeDeChamp < ApplicationRecord
type_champs.fetch(:dossier_link),
type_champs.fetch(:linked_drop_down_list),
type_champs.fetch(:drop_down_list),
type_champs.fetch(:textarea)
type_champs.fetch(:textarea),
type_champs.fetch(:cojo)
true
else
false

View file

@ -0,0 +1,2 @@
class TypesDeChamp::COJOTypeDeChamp < TypesDeChamp::TextTypeDeChamp
end

View file

@ -0,0 +1,14 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "http://demarches-simplifiees.fr/accreditation-cojo.schema.json",
"title": "Accreditation COJO",
"type": "object",
"properties": {
"firstName": { "type": "string" },
"lastName": { "type": "string" },
"individualExistance": {
"enum": ["Yes", "No"]
}
},
"required": ["individualExistance"]
}

View file

@ -0,0 +1,56 @@
class COJOService
include Dry::Monads[:result]
def call(accreditation_number:, accreditation_birthdate:)
result = API::Client.new.(url:,
json: {
accreditationNumber: accreditation_number.to_i,
birthdate: accreditation_birthdate&.strftime('%d/%m/%Y')
},
authorization_token:,
schema:,
method: :post)
case result
in Success(body:)
accreditation_success = body[:individualExistance] == 'Yes'
Success({
accreditation_success:,
accreditation_first_name: accreditation_success ? body[:firstName] : nil,
accreditation_last_name: accreditation_success ? body[:lastName] : nil
})
in Failure(code:, reason:) if code.in?(401..403)
Failure(API::Client::Error[:unauthorized, code, false, reason])
else
result
end
end
private
def schema
JSONSchemer.schema(Rails.root.join('app/schemas/accreditation-cojo.json'))
end
def url
"#{API_COJO_URL}/api/accreditation"
end
def authorization_token
rsa_private_key&.then { JWT.encode(jwt_payload, _1, 'RS256') }
end
def jwt_payload
{
iss: APPLICATION_NAME,
iat: Time.zone.now.to_i,
exp: 1.hour.from_now.to_i
}
end
def rsa_private_key
if ENV['COJO_JWT_RSA_PRIVATE_KEY'].present?
OpenSSL::PKey::RSA.new(ENV['COJO_JWT_RSA_PRIVATE_KEY'])
end
end
end

View file

@ -0,0 +1,9 @@
= champ.to_s
- if profile == 'instructeur'
%dl
%dt
Nom et prénom dans la base daccréditation :
%dd= "#{champ.accreditation_last_name} #{champ.accreditation_first_name}"
%dt
Nom et prénom saisie dans le dossier :
%dd= "#{champ.dossier.individual.nom} #{champ.dossier.individual.prenom}"

View file

@ -201,3 +201,9 @@ BANNER_MESSAGE=""
ADMINISTRATION_BANNER_MESSAGE=""
# for usager only
USAGER_BANNER_MESSAGE=""
# RSA private key to generate JWT tokens for communication with COJO services
COJO_JWT_RSA_PRIVATE_KEY=""
COJO_JWT_ISS=""
API_COJO_URL=""

View file

@ -6,6 +6,7 @@ API_EDUCATION_URL = ENV.fetch("API_EDUCATION_URL", "https://data.education.gouv.
API_GEO_URL = ENV.fetch("API_GEO_URL", "https://geo.api.gouv.fr")
API_PARTICULIER_URL = ENV.fetch("API_PARTICULIER_URL", "https://particulier.api.gouv.fr/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)
HELPSCOUT_API_URL = ENV.fetch("HELPSCOUT_API_URL", "https://api.helpscout.net/v2")
PIPEDRIVE_API_URL = ENV.fetch("PIPEDRIVE_API_URL", "https://api.pipedrive.com/v1")
SENDINBLUE_API_URL = ENV.fetch("SENDINBLUE_API_URL", "https://in-automate.sendinblue.com/api/v2")

View file

@ -18,7 +18,8 @@ features = [
:hide_instructeur_email,
:procedure_routage_api,
:groupe_instructeur_api_hack,
:rerouting
:rerouting,
:cojo_type_de_champ
]
def database_exists?

View file

@ -8,6 +8,7 @@ ActiveSupport::Inflector.inflections(:en) do |inflect|
# inflect.singular /^(ox)en/i, '\1'
# inflect.irregular 'person', 'people'
# inflect.uncountable %w( fish sheep )
inflect.acronym 'COJO'
inflect.acronym 'API'
inflect.acronym 'ASN1'
inflect.acronym 'IP'

View file

@ -50,6 +50,7 @@ en:
pole_emploi: 'Pôle emploi status'
mesri: "Data from Ministère de lEnseignement Supérieur, de la Recherche et de lInnovation"
epci: "EPCI"
cojo: "Accreditation Paris 2024"
errors:
type_de_champ:
attributes:

View file

@ -50,6 +50,7 @@ fr:
pole_emploi: 'Situation Pôle emploi'
mesri: "Données du Ministère de lEnseignement Supérieur, de la Recherche et de lInnovation"
epci: "EPCI"
cojo: "Accréditation Paris 2024"
errors:
type_de_champ:
attributes:

View file

@ -239,6 +239,10 @@ FactoryBot.define do
value { 'W173847273' }
end
factory :champ_cojo, class: 'Champs::COJOChamp' do
type_de_champ { association :type_de_champ_cojo, procedure: dossier.procedure }
end
factory :champ_repetition, class: 'Champs::RepetitionChamp' do
type_de_champ { association :type_de_champ_repetition, procedure: dossier.procedure }

View file

@ -181,6 +181,9 @@ FactoryBot.define do
factory :type_de_champ_epci do
type_champ { TypeDeChamp.type_champs.fetch(:epci) }
end
factory :type_de_champ_cojo do
type_champ { TypeDeChamp.type_champs.fetch(:cojo) }
end
factory :type_de_champ_repetition do
type_champ { TypeDeChamp.type_champs.fetch(:repetition) }

View file

@ -0,0 +1,4 @@
{
"individualExistance": "maybe",
"firstName": "Florence"
}

View file

@ -0,0 +1,5 @@
{
"individualExistance": "No",
"firstName": "Not found",
"lastName": "Not found"
}

View file

@ -0,0 +1,5 @@
{
"individualExistance": "Yes",
"firstName": "Florence",
"lastName": "Griffith-Joyner"
}

View file

@ -14,9 +14,9 @@ describe '20220705164551_remove_unused_champs' do
describe 'remove_unused_champs' do
it "with bad champs" do
expect(Champ.where(dossier: dossier).count).to eq(40)
expect(Champ.where(dossier: dossier).count).to eq(41)
run_task
expect(Champ.where(dossier: dossier).count).to eq(39)
expect(Champ.where(dossier: dossier).count).to eq(40)
end
end
end

View file

@ -0,0 +1,50 @@
describe Champs::COJOChamp, type: :model do
let(:champ) { build(:champ_cojo, accreditation_number:, accreditation_birthdate:) }
let(:external_id) { nil }
let(:stub) { stub_request(:post, url).with(body: { accreditationNumber: accreditation_number, birthdate: accreditation_birthdate }).to_return(body:, status:) }
let(:url) { COJOService.new.send(:url) }
let(:body) { Rails.root.join('spec', 'fixtures', 'files', 'api_cojo', "accreditation_#{response_type}.json").read }
let(:status) { 200 }
let(:response_type) { 'yes' }
let(:accreditation_number) { 123456 }
let(:accreditation_birthdate) { '21/12/1959' }
describe 'fetch_external_data' do
subject { stub; champ.fetch_external_data }
context 'success (yes)' do
it { expect(subject.value!).to eq({ accreditation_success: true, accreditation_first_name: 'Florence', accreditation_last_name: 'Griffith-Joyner' }) }
end
context 'success (no)' do
let(:response_type) { 'no' }
it { expect(subject.value!).to eq({ accreditation_success: false, accreditation_first_name: nil, accreditation_last_name: nil }) }
end
context 'failure (schema)' do
let(:response_type) { 'invalid' }
it {
expect(subject.failure.retryable).to be_falsey
expect(subject.failure.reason).to be_a(API::Client::SchemaError)
}
end
context 'failure (http 500)' do
let(:status) { 500 }
let(:response_type) { 'invalid' }
it {
expect(subject.failure.retryable).to be_truthy
expect(subject.failure.reason).to be_a(API::Client::HTTPError)
}
end
context 'failure (http 401)' do
let(:status) { 401 }
let(:response_type) { 'invalid' }
it {
expect(subject.failure.retryable).to be_falsey
expect(subject.failure.reason).to be_a(API::Client::HTTPError)
}
end
end
end

View file

@ -88,7 +88,8 @@ describe ProcedureExportService do
"text",
"epci",
"epci (Code)",
"epci (Département)"
"epci (Département)",
"cojo"
]
end
@ -198,7 +199,8 @@ describe ProcedureExportService do
"text",
"epci",
"epci (Code)",
"epci (Département)"
"epci (Département)",
"cojo"
]
end
@ -291,7 +293,8 @@ describe ProcedureExportService do
"text",
"epci",
"epci (Code)",
"epci (Département)"
"epci (Département)",
"cojo"
]
end