Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Quelques fixes pour fiabiliser l'algo #52

Merged
merged 11 commits into from
Dec 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions config/locales/fr/mon-devis-sans-oublis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ fr:
errors:
devis_manquant: 'La mention explicite "devis" est obligatoire. Les bons de commande et propositions commerciales ne sont pas acceptés.'
numero_devis_manquant: "Un devis doit avoir un numéro unique."
date_validite_manquant: "Attention, si la date de validité n'est pas indiquée, elle est de 3 mois par défaut."
date_devis_manquant: "La date d'édition du devis est obligatoire."
date_chantier_manquant: "Nous recommandons de mettre la date de début de chantier prévue."
date_pre_visite_manquant: "La date de pré visite technique est fortement recommandée, notamment dans le cadre des CEE."

# Informations de l'artisan
pro_raison_sociale_manquant: "Le nom de votre structure doit être mentionné."
Expand All @@ -29,6 +33,7 @@ SNC, SAS."
siret_manquant_infos : "Le Numéro SIRET (système d’identification du répertoire des établissements) est une série de 14 chiffres. Il correspond au numéro SIREN (Système d’Identification du Répertoire des Entreprises, à 9 chiffres) délivré lors de la création de l'entreprise, suivi du numéro NIC (numéro interne de classement)."
siret_format_erreur: "Attention, il semblerait que le SIREN ait été utilisé à la place du SIRET. Le SIRET est une série de 14 chiffres."
pro_adresse_manquant: "L'adresse du siège social est requise"
rge_manquant: "Pour obtenir des aides, il faut avoir le label RGE du geste en question. Merci de préciser le numéro de certification."

# Information du client :
client_prenom_manquant: "Le prénom du client est requis. Attention, le devis doit être établi au nom de la personne ayant fait la demande d’aide."
Expand Down
4 changes: 2 additions & 2 deletions lib/quote_reader/naive_text.rb
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ def version
FRENCH_CHARACTER_REGEX = /[\wÀ-ÖØ-öø-ÿ]/i
PHONE_REGEX = /(?:\(?\+?33\)?)?\s?(?:[\s.]*\d\d){5}/i # TODO: find better
RCS_REGEX = /R\.?C\.?S\.?(?:\s+[A-Za-zÀ-ÖØ-öø-ÿ\s-]+)?(?:\s+[AB])?\s+\d{9}/i
URI_REGEX = %r{(?:https?|ftp)://[^\s/$.?#].[^\s]*}i
URI_REGEX = %r{(?:https?|ftp)://(?:www\.)?[^\s/$.?#].[^\s]*|www\.[^\s/$.?#].[^\s]*}i

def self.find_adresses(text)
(text.scan(/Adresse\s*:\s*(#{FRENCH_CHARACTER_REGEX}+)/i).flatten +
Expand Down Expand Up @@ -114,7 +114,7 @@ def self.find_ibans(text)
def self.find_label_numbers(text)
# Warning : insure caracter before not match the IBAN
text.scan(
%r{(?:\A|.*#{BETWEEN_LABEL_VALUE_REGEX})((?:(?:CPLUS|QB|QPAC|QPV|QS|VPLUS)/|(?:R|E-)?E)\d{5,6})}i
%r{(?:\A|.*#{BETWEEN_LABEL_VALUE_REGEX})((?:(?:CPLUS|QB|QPAC|QPV|QS|VPLUS)\s*/\s*|(?:R|E-)?E)\s*\d{5,6})}i
).flatten.filter_map { |e| e&.strip }.uniq
end

Expand Down
6 changes: 4 additions & 2 deletions lib/quote_reader/qa.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class Qa < Text
def read
return {} if text.blank?

@read_attributes = llm_read_attributes
llm_read_attributes
end

def version
Expand All @@ -29,8 +29,10 @@ def llm_read_attributes
ErrorNotifier.notify(e)
end

@read_attributes = mistral.read_attributes
@read_attributes = TrackingHash.new(mistral.read_attributes)
@result = mistral.result

read_attributes
end

def prompt
Expand Down
4 changes: 2 additions & 2 deletions lib/quote_validator/chauffage.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ def validate_chauffage(geste, error)
error << "puissance_manquant" if geste[:puissance].blank?
error << "marque_isolation_manquant" if geste[:marque].blank?
error << "reference_isolation_manquant" if geste[:reference].blank?
error << "etas_chauffage_manquant" if geste[:ETAS].blank # en %
error << "etas_chauffage_manquant" if geste[:ETAS].blank? # en %

# TODO: à challenger
@warnings << "remplacement_chaudiere_condensation_manquant" unless geste[:remplacement_chaudiere_condensation]
Expand Down Expand Up @@ -156,7 +156,7 @@ def validate_pac(geste)
# ≥ 126% si basse T
# ≥ 111% si Haute T

error << "cop_chauffage_manquant" if geste[:cop].blank? # TODO: V1 Check if SCOP is required too.
error << "cop_chauffage_manquant" if geste[:COP].blank? # TODO: V1 Check if SCOP is required too.

error
end
Expand Down
34 changes: 25 additions & 9 deletions lib/quote_validator/global.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def validate!
def validate_admin
# mention devis présente ou non, quote[:mention_devis] est un boolean
@errors << "devis_manquant" unless quote[:mention_devis] || quote[:devis].present?
@errors << "numero_devis_manquant" if quote[:numero_devis].present?
@errors << "numero_devis_manquant" if quote[:numero_devis].blank?

validate_dates
validate_pro
Expand All @@ -31,12 +31,28 @@ def validate_admin
# date d'emission, date de pré-visite (CEE uniquement ?),
# validité (par défaut 3 mois -> Juste un warning),
# Date de début de chantier (CEE uniquement)
def validate_dates; end
def validate_dates
# date_devis
@errors << "date_devis_manquant" if quote[:date_devis].blank?

# date_debut_chantier
@warnings << "date_chantier_manquant" if quote[:date_chantier].blank?

# date_pre_visite
@errors << "date_pre_visite_manquant" if quote[:date_pre_visite].blank?

# validite
@warnings << "date_validite_manquant" unless quote[:validite]
end

# V0 on check la présence - attention devrait dépendre du geste, à terme,
# on pourra utiliser une API pour vérifier la validité
# Attention, souvent on a le logo mais rarement le numéro RGE.
def validate_rge; end
def validate_rge
@pro = quote[:pro] ||= TrackingHash.new
rge_labels = @pro[:labels]
@errors << "rge_manquant" if rge_labels.blank?
end

# doit valider les mentions administratives associées à l'artisan
# rubocop:disable Metrics/AbcSize
Expand All @@ -56,7 +72,7 @@ def validate_pro
@errors << "siret_manquant" if @pro[:siret].blank?
# beaucoup de confusion entre SIRET (14 chiffres pour identifier un etablissement)
# et SIREN (9 chiffres pour identifier une entreprise)
@errors << "siret_format_erreur" if @pro[:siret]&.length != 14 && @pro[:siret]&.length&.positive?
@errors << "siret_format_erreur" if @pro[:siret]&.gsub(/\s+/, "")&.length != 14 && @pro[:siret]&.length&.positive?
validate_pro_address
end
# rubocop:enable Metrics/PerceivedComplexity
Expand Down Expand Up @@ -109,20 +125,20 @@ def validate_address(address, type)
# rubocop:disable Metrics/CyclomaticComplexity
# rubocop:disable Metrics/MethodLength
def validate_works
works = quote[:gestes] || []
isolation = Isolation.new(quote)
menuiserie = Menuiserie.new(quote)
chauffage = Chauffage.new(quote)
eau_chaude = EauChaude.new(quote)
ventilation = Ventilation.new(quote)

@errors += works.flat_map do |geste| # rubocop:disable Metrics/BlockLength
gestes = quote[:gestes] || []
@errors += gestes.flat_map do |geste| # rubocop:disable Metrics/BlockLength
case geste[:type]

# ISOLATION
when "isolation_mur_ite"
isolation.validate_isolation_ite(geste)
when "isolation_combles_perdues"
when "isolation_comble_perdu", "isolation_combles_perdues"
isolation.validate_isolation_combles(geste)
when "isolation_rampants-toiture"
isolation.validate_isolation_rampants(geste)
Expand Down Expand Up @@ -150,13 +166,13 @@ def validate_works
chauffage.validate_poele_insert(geste)
when "systeme_solaire_combine"
chauffage.validate_systeme_solaire_combine(geste)
when "pac"
when "pac", "pac_air_eau"
chauffage.validate_pac(geste)

# EAU CHAUDE SANITAIRE
when "chauffe_eau_solaire_individuel"
eau_chaude.validate_cesi(geste)
when "chauffe_eau_thermodynamique"
when "chauffe_eau_thermo", "chauffe_eau_thermodynamique"
eau_chaude.validate_chauffe_eau_thermodynamique(geste)

# VENTILATION
Expand Down
34 changes: 32 additions & 2 deletions lib/tracking_hash.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,47 @@

# TrackingHash is a subclass of Hash that tracks the keys that are accessed.
class TrackingHash < Hash
# rubocop:disable Metrics/MethodLength
def initialize(constructor = {})
super()

@keys_accessed = Set.new

return unless constructor.is_a?(Hash)

constructor&.each do |key, value|
self[key] = value.is_a?(Hash) ? TrackingHash.new(value) : value
self[key] = if value.is_a?(Hash)
TrackingHash.new(value)
elsif value.is_a?(Array)
value.map { |subvalue| TrackingHash.new(subvalue) }
else
value
end
end
end
# rubocop:enable Metrics/MethodLength

def [](key)
@keys_accessed.add(key)
super || super(key.to_s)

unless key?(key)
return super(key.to_s) if key.is_a?(Symbol)

return super(key.to_sym) if key.is_a?(String)
end

super
end

def dig(*keys)
current = self
keys.each do |key|
return nil unless current.is_a?(Hash) || current.is_a?(Array)

current = current[key] # Reuse overwritten methods
end

current
end

def keys_accessed
Expand Down
30 changes: 30 additions & 0 deletions spec/lib/tracking_hash_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,36 @@
hash = described_class.new(a: 1)
expect(hash[:a]).to eq 1
end

# rubocop:disable RSpec/MultipleExpectations
it "works like an indifferent Hash" do
expect(described_class.new(a: 1)["a"]).to eq 1
expect(described_class.new("a" => 1)[:a]).to eq 1
end
# rubocop:enable RSpec/MultipleExpectations

context "with an empty Hash and unknown key" do
it "works as usual" do
expect(described_class.new[:unknown_key]).to be_nil
end
end
end

describe "#dig" do
context "with an empty Hash" do
it "works as usual" do
hash = described_class.new
expect(hash.dig(nil)).to be_nil # rubocop:disable Style/SingleArgumentDig
end
end

# rubocop:disable RSpec/MultipleExpectations
it "works like with quotes and symbols" do
hash = described_class.new(subhash_symbol: [{ "key" => 1 }])
expect(hash.dig("subhash_symbol", 0, "key")).to eq 1
expect(hash.dig(:subhash_symbol, 0, :key)).to eq 1
end
# rubocop:enable RSpec/MultipleExpectations
end

describe "#keys_accessed" do
Expand Down
1 change: 1 addition & 0 deletions spec/models/quote_file_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
quote_file = described_class.find_or_create_file(file, file.original_filename)

expect(quote_file).to be_persisted
expect(quote_file.file).to be_attached

quote_file.reload
expect(quote_file.file).to be_attached
Expand Down
4 changes: 2 additions & 2 deletions spec/requests/api/v1/quote_checks_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@

# rubocop:disable RSpec/MultipleExpectations
it "renders a successful response" do
get api_v1_quote_check_url(quote_check), as: :json
get api_v1_quote_check_url(quote_check), as: :json, headers: basic_auth_header
expect(response).to be_successful
expect(json.fetch("status")).to eq("invalid")
end
Expand All @@ -59,7 +59,7 @@

# rubocop:disable RSpec/MultipleExpectations
it "returns a direct error response" do
get api_v1_quote_check_url(quote_check), as: :json
get api_v1_quote_check_url(quote_check), as: :json, headers: basic_auth_header
expect(response).to be_successful
expect(json.fetch("status")).to eq("invalid")
expect(json.fetch("errors")).to include("file_reading_error")
Expand Down
Loading