Skip to content

Commit

Permalink
Valider le format du numéro ANTS avant de consulter leur API (#4205)
Browse files Browse the repository at this point in the history
* Reproduce crash with spec

* Green spec, little refactor

* Détecter le mauvais format de numéro ANTS avant d'appeler l'API

* Keep it minimal

* Cleanup merge

* Considérer l'API ANTS comme fiable

* Déplacer la validation sur User

* Introduce ANTS helper methods and clarify specs

* Fix User model spec

* Share config and i18n between User and BeneficiaireForm

* Please rubocop

* Move to concern

* Use I18n for unexpected API error too

* Revert back to using a validator that takes a record

* Feels safer

* Cleanup

* Bring back old code to minimize diff

* Fix merge

* Use explicit application_ids to prevent accidental matching

* Use spec helpers

* Please rubocop

* Move ants_api.rb to app/lib

* Fix merge
  • Loading branch information
francois-ferrandis committed Apr 24, 2024
1 parent 1a7e14b commit 1f6f5b2
Show file tree
Hide file tree
Showing 13 changed files with 218 additions and 167 deletions.
5 changes: 4 additions & 1 deletion app/form_models/prescripteur_rdv_wizard.rb
Expand Up @@ -58,6 +58,9 @@ def find_or_create_user

@user.skip_confirmation_notification! # Désactivation du mail Devise de confirmation de compte
@user.created_through = "prescripteur"
@user.user_profiles.find_or_initialize_by(organisation_id: rdv.motif.organisation_id).save!
User.transaction do
@user.save!
@user.user_profiles.find_or_create_by!(organisation_id: rdv.motif.organisation_id)
end
end
end
1 change: 1 addition & 0 deletions app/form_models/user_rdv_wizard.rb
Expand Up @@ -92,6 +92,7 @@ class Step1 < Base
ants_pre_demande_number: @user_attributes[:ants_pre_demande_number],
ignore_benign_errors: @user_attributes[:ignore_benign_errors]
)
errors.merge!(@user)
end

def phone_number_present_for_motif_by_phone
Expand Down
17 changes: 16 additions & 1 deletion app/services/ants_api.rb → app/lib/ants_api.rb
Expand Up @@ -4,6 +4,21 @@ class AntsApi

class ApiRequestError < StandardError; end

VALIDATED = "validated".freeze

CONSUMED = "consumed".freeze
DECLARED = "declared".freeze
UNKNOWN = "unknown".freeze
EXPIRED = "expired".freeze

# https://api-coordination.rendezvouspasseport.ants.gouv.fr/docs#/Application%20Ids%20-%20%C3%A9diteurs/get_status_api_status_get
ERROR_STATUSES = {
CONSUMED => "correspond à un dossier déjà instruit",
DECLARED => "n'est pas officiellement reconnu par l'ANTS",
UNKNOWN => "n'est pas reconnu par l'ANTS",
EXPIRED => "correspond à un dossier expiré",
}.freeze

class << self
def status(application_id:, timeout: nil)
response_body = request(:get, "status", params: { application_ids: application_id }, timeout: timeout)
Expand Down Expand Up @@ -74,7 +89,7 @@ def request(method, resource, params:, timeout: nil)
raise(ApiRequestError, "code:#{response.response_code}, body:#{response.response_body}")
end

response.body.empty? ? {} : JSON.parse(response.body)
JSON.parse(response.body)
end

def load_appointments(application_id)
Expand Down
26 changes: 9 additions & 17 deletions app/models/concerns/user/ants.rb
Expand Up @@ -4,6 +4,11 @@ module User::Ants
def self.validate_ants_pre_demande_number(user:, ants_pre_demande_number:, ignore_benign_errors:)
return if ants_pre_demande_number.blank?

unless ants_pre_demande_number.match?(/\A[A-Za-z0-9]{10}\z/)
user.errors.add(:ants_pre_demande_number, :invalid_format)
return
end

application_hash = AntsApi.status(application_id: ants_pre_demande_number, timeout: 4)

status = application_hash["status"]
Expand All @@ -16,13 +21,13 @@ def self.validate_ants_pre_demande_number(user:, ants_pre_demande_number:, ignor
end

else
user.errors.add(:base, error_message(application_hash["status"]))
user.errors.add(:ants_pre_demande_number, AntsApi::ERROR_STATUSES.fetch(status))
end
rescue AntsApi::ApiRequestError, Typhoeus::Errors::TimeoutError => e
# Si l'api de l'ANTS renvoie une erreur ou un timeout, on ne veut pas bloquer la prise de rendez-vous
# pour l'usager, donc on considère le numéro comme valide.
# Si l'API de l'ANTS est fiable, donc si elle renvoie une erreur ou un timeout,
# on préfère bloquer la réservation et logguer l'erreur.
user.errors.add(:ants_pre_demande_number, :unexpected_api_error)
Sentry.capture_exception(e)
nil
end

def self.warning_message(appointment)
Expand All @@ -33,19 +38,6 @@ def self.warning_message(appointment)
)
end

def self.error_message(status)
case status
when "consumed"
"Ce numéro de pré-demande ANTS correspond à un dossier déjà instruit"
when "unknown"
"Ce numéro de pré-demande ANTS est inconnu"
when "expired"
"Ce numéro de pré-demande ANTS a expiré"
else
"Ce numéro de pré-demande ANTS est invalide"
end
end

def syncable_with_ants?
return if ants_pre_demande_number.blank?

Expand Down
11 changes: 1 addition & 10 deletions app/models/user.rb
Expand Up @@ -66,22 +66,13 @@ def self.search_options
# Validations
validates :last_name, :first_name, :created_through, presence: true
validates :number_of_children, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true
validates(
:ants_pre_demande_number,
format: {
with: /\A[A-Za-z0-9]+\z/,
message: "Seulement des nombres et lettres",
if: -> { ants_pre_demande_number.present? },
}
)
validates :ants_pre_demande_number, length: { is: 10 }, if: -> { ants_pre_demande_number.present? }

validate :birth_date_validity

# Hooks
before_save :set_email_to_null_if_blank
# voir Ants::AppointmentSerializerAndListener pour d'autres callbacks
before_save -> { ants_pre_demande_number.upcase! }, if: -> { ants_pre_demande_number.present? }
before_validation -> { ants_pre_demande_number.upcase! }, if: -> { ants_pre_demande_number.present? }

# Scopes
default_scope { where(deleted_at: nil) }
Expand Down
5 changes: 3 additions & 2 deletions app/views/prescripteur_rdv_wizard/new_beneficiaire.html.slim
Expand Up @@ -19,13 +19,14 @@ main.container
- if @rdv_wizard.rdv.requires_ants_predemande_number?
.row
.col-12
= f.input :ants_pre_demande_number, label: "Numéro de pré-demande ANTS", required: true, input_html: {style: "text-transform: uppercase;"}
= f.input :ants_pre_demande_number, required: true, input_html: {style: "text-transform: uppercase;"}

= render("model_errors", model: @beneficiaire, f: f)
.form-group
.row
.col-12
= f.input :phone_number, as: :tel, placeholder: "06134567890", hint: "Un SMS de confirmation et un SMS de rappel seront envoyés à ce numéro"

= render("model_errors", model: @beneficiaire, f: f)

.form-group.mb-0.text-center
= f.submit "Confirmer le rendez-vous", class: "btn btn-primary"
15 changes: 14 additions & 1 deletion config/locales/models/user.fr.yml
Expand Up @@ -18,7 +18,7 @@ fr:
notify_by_email: "Accepte les notifications par email"
created_through: "Origine du compte"
notes: Remarques
ants_pre_demande_number: Numéro de pré-demande ANTS
ants_pre_demande_number: &ants_pre_demande_number_label Numéro de pré-demande ANTS
user/logements:
sdf: Sans domicile fixe
heberge: Hébergé
Expand Down Expand Up @@ -53,6 +53,9 @@ fr:
too_common: "Ce mot de passe fait partie d'une liste de mots de passe fréquemment utilisés et ne permet donc pas d'assurer la sécurité de votre compte. Veuillez en choisir un autre."
too_short:
other: "Pour assurer la sécurité de votre compte, votre mot de passe doit faire au moins %{count} caractères"
ants_pre_demande_number: &ants_pre_demande_number_errors
invalid_format: doit comporter 10 chiffres et lettres
unexpected_api_error: n'a pas pu être validé à cause d'une erreur inattendue. Merci de réessayer dans 30 secondes.
warnings:
models:
user:
Expand All @@ -66,3 +69,13 @@ fr:
agent:
ants_pre_demande_number_html: Pour accélérer la démarche de l'usager, nous vous recommandons très fortement de pré-remplir son dossier sur le <a href="https://passeport.ants.gouv.fr/demarches-en-ligne" target="_blank">site de l'ANTS</a>, et d'indiquer son numéro de pré-demande.

# BeneficiaireForm
activemodel:
attributes:
beneficiaire_form:
ants_pre_demande_number: *ants_pre_demande_number_label
errors:
models:
beneficiaire_form:
attributes:
ants_pre_demande_number: *ants_pre_demande_number_errors
Expand Up @@ -20,15 +20,7 @@

context "ants_pre_demander number is validated and has no appointment declared yet" do
before do
stub_request(:get, %r{https://int.api-coordination.rendezvouspasseport.ants.gouv.fr/api/status}).to_return(
status: 200,
body: {
ants_pre_demande_number => {
status: "validated",
appointments: [],
},
}.to_json
)
stub_ants_status("1122334455")
end

it "creates user with no warning" do
Expand All @@ -44,20 +36,15 @@

context "ants_pre_demander number is validated but already has appointments" do
before do
stub_request(:get, %r{https://int.api-coordination.rendezvouspasseport.ants.gouv.fr/api/status}).to_return(
status: 200,
body: {
ants_pre_demande_number => {
status: "validated",
appointments: [
{
management_url: "https://gerer-rdv.com",
meeting_point: "Mairie de Sannois",
appointment_date: "2023-04-03T08:45:00",
},
],
stub_ants_status(
"1122334455",
appointments: [
{
management_url: "https://gerer-rdv.com",
meeting_point: "Mairie de Sannois",
appointment_date: "2023-04-03T08:45:00",
},
}.to_json
]
)
end

Expand All @@ -77,23 +64,15 @@

context "ants_pre_demander number is consumed (dossier déjà envoyé et instruit en préfecture)" do
before do
stub_request(:get, %r{https://int.api-coordination.rendezvouspasseport.ants.gouv.fr/api/status}).to_return(
status: 200,
body: {
ants_pre_demande_number => {
status: "consumed",
appointments: [],
},
}.to_json
)
stub_ants_status("1122334455", status: "consumed")
end

it "prevents agent from creating the user / RDV" do
fill_in :user_first_name, with: "Marco"
fill_in :user_last_name, with: "Lebreton"
fill_in :user_ants_pre_demande_number, with: ants_pre_demande_number
click_button "Créer"
expect(page).to have_content("Ce numéro de pré-demande ANTS correspond à un dossier déjà instruit")
expect(page).to have_content("Numéro de pré-demande ANTS correspond à un dossier déjà instruit")
expect(page).not_to have_content("Confirmer en ignorant les avertissements")
end
end
Expand Down
Expand Up @@ -28,7 +28,6 @@ def fill_up_prescripteur_and_user
fill_in "Votre email professionnel", with: "alex@prescripteur.fr"
fill_in "Votre numéro de téléphone", with: "0611223344"
click_on "Continuer"

expect(page).to have_content("Prescripteur : Alex PRESCRIPTEUR")
fill_in "Prénom", with: "Patricia"
fill_in "Nom", with: "Duroy"
Expand All @@ -44,10 +43,7 @@ def fill_up_prescripteur_and_user

context "success scenario (ants_pre_demander number is validated and has no appointment declared yet)" do
before do
stub_request(:get, %r{https://int.api-coordination.rendezvouspasseport.ants.gouv.fr/api/status}).to_return(
status: 200,
body: { ants_pre_demande_number => { status: "validated", appointments: [] } }.to_json
)
stub_ants_status("1122334455")
end

it "allows booking a rdv for the given ants_pre_demander" do
Expand All @@ -65,20 +61,15 @@ def fill_up_prescripteur_and_user

context "ants_pre_demander number is validated but already has appointments" do
before do
stub_request(:get, %r{https://int.api-coordination.rendezvouspasseport.ants.gouv.fr/api/status}).to_return(
status: 200,
body: {
ants_pre_demande_number => {
status: "validated",
appointments: [
{
management_url: "https://gerer-rdv.com",
meeting_point: "Mairie de Sannois",
appointment_date: "2023-04-03T08:45:00",
},
],
stub_ants_status(
"1122334455",
appointments: [
{
management_url: "https://gerer-rdv.com",
meeting_point: "Mairie de Sannois",
appointment_date: "2023-04-03T08:45:00",
},
}.to_json
]
)
end

Expand All @@ -102,9 +93,26 @@ def fill_up_prescripteur_and_user

context "ants_pre_demander number is consumed (dossier déjà envoyé et instruit en préfecture)" do
before do
stub_request(:get, %r{https://int.api-coordination.rendezvouspasseport.ants.gouv.fr/api/status}).to_return(
status: 200,
body: { ants_pre_demande_number => { status: "consumed", appointments: [] } }.to_json
stub_ants_status("1122334455", status: "consumed")
end

it "prevents from creating the user / RDV" do
visit creneaux_url
click_on "Je suis un prescripteur qui oriente un bénéficiaire"

fill_up_prescripteur_and_user
click_on "Confirmer le rendez-vous"

expect(page).to have_content("Numéro de pré-demande ANTS correspond à un dossier déjà instruit")
expect(page).not_to have_content("Confirmer en ignorant les avertissements")
end
end

context "ANTS responds with an unexpected error" do
before do
stub_request(:get, "https://int.api-coordination.rendezvouspasseport.ants.gouv.fr/api/status?application_ids=1122334455").to_return(
status: 500,
body: "Internal Server Error"
)
end

Expand All @@ -115,7 +123,22 @@ def fill_up_prescripteur_and_user
fill_up_prescripteur_and_user
click_on "Confirmer le rendez-vous"

expect(page).to have_content("Ce numéro de pré-demande ANTS correspond à un dossier déjà instruit")
expect(page).to have_content("Numéro de pré-demande ANTS n'a pas pu être validé à cause d'une erreur inattendue. Merci de réessayer dans 30 secondes.")
expect(page).not_to have_content("Confirmer en ignorant les avertissements")
end
end

context "ants_pre_demander number is invalid (too short)" do
let(:ants_pre_demande_number) { "123" }

it "prevents from creating the user / RDV" do
visit creneaux_url
click_on "Je suis un prescripteur qui oriente un bénéficiaire"

fill_up_prescripteur_and_user
click_on "Confirmer le rendez-vous"

expect(page).to have_content("Numéro de pré-demande ANTS doit comporter 10 chiffres et lettres")
expect(page).not_to have_content("Confirmer en ignorant les avertissements")
end
end
Expand Down

0 comments on commit 1f6f5b2

Please sign in to comment.