cpro-client 0.2.5 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fcf97fbee94114c1241fb76a3b3ec620dd7d8c2e40435bebfe146ce7b65a3e92
4
- data.tar.gz: 0b3e4f80fe36110c60a46d7adc951cb0bd662093a30e39a0f19fb9e4c252096d
3
+ metadata.gz: 1f1452657739318ca5d2a71a6673d046dea03e4f5e3b00befc41ae210fcf3ee4
4
+ data.tar.gz: b36804bf664635e9d5f510fe8a8ed2e52a6aeb1753b9de3bc3b919475d3e51cb
5
5
  SHA512:
6
- metadata.gz: 4f010893e86d5f3dec2742aca27ea56606f6facb294c23cc613f91073d4ebf3b3f56dc1fdea650121b76a706680a65bd4921c875230a0e00beb428d4306f7876
7
- data.tar.gz: 65724ca9625aa0328e6d3b14f10005f087614e589587f73e706b17d515b1091f3633f960bf9036a8031e30abdab360814856d076de551eaa0f5a860759b3a0a9
6
+ metadata.gz: 918adeba480ffaaec7508e21e68c31799708e568fb4dc5f2678072ab9f10971f31a03c8af893e57548cce0d5c93b9ad2b9d5387a50a7d629f40f945dfb16b93a
7
+ data.tar.gz: 299ffdbbc8b8c9f25b24c09d1577fdd4e0d1f3b9bc9f3b0eed7c6d607f428e86e87d1a1978bb55c10ab6e450bdb24c996b5909673113031cfb375bb65ab30255
data/CHANGELOG.md CHANGED
@@ -1,6 +1,24 @@
1
1
  ## [Unreleased]
2
2
 
3
3
 
4
+ ## [0.2.6] - 2026-09-05
5
+
6
+ ### Modifié
7
+ - `Cpro::Resources::Annuaire#find_identifiant_actif` devient **`#find_identifiant_adressage`**, et
8
+ ne filtre plus sur le statut de plateforme : il le fait départager. À proximité égale une adresse
9
+ active l'emporte, mais faute d'adresse active la plus proche est rendue quand même.
10
+
11
+ L'ancien comportement interdisait de facturer un destinataire non raccordé, alors que le dossier
12
+ de spécifications externes pose l'inverse : une telle facture est *déposée*, avec le motif
13
+ `NON_TRANSMISE`, et ce statut « permet à l'entité publique de justifier qu'elle a bien émis une
14
+ facture électronique et qu'elle a donc respecté ses obligations réglementaires ». L'émetteur doit
15
+ remettre un duplicata à son client, mais l'obligation réglementaire est remplie — la gem n'a pas
16
+ à s'y opposer.
17
+
18
+ L'appelant lit `plateforme_active?` sur la ligne rendue pour distinguer les deux cas, et `nil` ne
19
+ signifie plus qu'une chose : aucune adresse à aucun niveau.
20
+
21
+
4
22
  ## [0.2.5] - 2026-09-05
5
23
 
6
24
  ### Ajouté
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- cpro-client (0.2.5)
4
+ cpro-client (0.2.6)
5
5
  faraday (>= 0.9, < 3.0)
6
6
 
7
7
  GEM
data/README.md CHANGED
@@ -142,27 +142,40 @@ BT-49, l'adresse électronique de l'acheteur, ne se transmet pas à l'API : elle
142
142
  Factur-X, et c'est d'elle que le PPF déduit l'acheminement. Une facture qui la porte mal est
143
143
  rejetée `REJ_ADR` — « le destinataire n'est pas trouvable » — plusieurs minutes après le dépôt.
144
144
 
145
- `find_identifiant_actif` fait la résolution en un appel. Il accepte les quatre formes d'une valeur
146
- d'adressage — SIREN, SIRET, SIRET_SERVICE, SIREN_SIRET_SERVICE — et remonte la hiérarchie
147
- SIREN → SIRET → service jusqu'à trouver une ligne dont la plateforme est active :
145
+ `find_identifiant_adressage` fait la résolution en un appel. Il accepte les quatre formes d'une
146
+ valeur d'adressage — SIREN, SIRET, SIRET_SERVICE, SIREN_SIRET_SERVICE — et remonte la hiérarchie
147
+ SIREN → SIRET → service pour rendre l'adresse la plus proche que l'annuaire connaisse :
148
148
 
149
149
  ```ruby
150
- ligne = client.annuaire.find_identifiant_actif("71915767420316")
150
+ ligne = client.annuaire.find_identifiant_adressage("71915767420316")
151
151
  ligne.to_s # => "719157674_71915767420316", à porter en BT-49
152
- ligne.identifiant_routage # => "" l'adresse de base de l'établissement
152
+ ligne.plateforme_active? # => la facture sera-t-elle transmise ?
153
153
 
154
- client.annuaire.find_identifiant_actif("71915767420316_SERVICE_INEXISTANT").to_s
154
+ client.annuaire.find_identifiant_adressage("71915767420316_SERVICE_INEXISTANT").to_s
155
155
  # => "719157674_71915767420316" — service inconnu, on retombe sur son parent
156
156
  ```
157
157
 
158
- `nil` signifie qu'aucun niveau n'est adressable, donc qu'on ne peut pas facturer. Pour savoir
159
- *pourquoi*, `find_lignes_adressage` rend les mêmes lignes sans filtrer : une unité légale inconnue
160
- lève une `Cpro::NotFoundError`, tandis qu'une unité connue mais non raccordée rend ses lignes avec
161
- `plateforme_active?` à faux.
158
+ **Le statut de plateforme départage, il n'exclut pas.** À égalité de proximité, une adresse dont la
159
+ plateforme est active l'emporte ; mais si aucune ne l'est, l'adresse est rendue quand même. Elle
160
+ reste la bonne : une facture destinée à un non-raccordé est bien *déposée*, avec le motif
161
+ `NON_TRANSMISE`, et ce statut « permet à l'entité publique de justifier qu'elle a bien émis une
162
+ facture électronique et qu'elle a donc respecté ses obligations réglementaires ». L'émetteur devra
163
+ remettre un duplicata à son client, mais l'obligation est remplie.
162
164
 
163
- Deux limites à connaître. La résolution ne redescend jamais vers les enfants : un SIREN nu ne
164
- remonte rien si la structure n'a d'adresses qu'au niveau SIRET. Et une adresse active n'est pas une
165
- promesse d'acheminement elle garantit que le destinataire est identifiable, pas qu'il recevra.
165
+ L'appelant lit donc `plateforme_active?` sur la ligne rendue pour savoir dans quel cas il est :
166
+
167
+ | Résultat | Ce qui se passera |
168
+ | --- | --- |
169
+ | ligne, `plateforme_active?` vrai | la facture est transmise au destinataire |
170
+ | ligne, `plateforme_active?` faux | déposée et conforme, mais non transmise — duplicata à prévoir |
171
+ | `nil` | aucune adresse à aucun niveau : on ne peut pas facturer |
172
+
173
+ Pour distinguer « unité légale inconnue » de « connue mais sans adresse exploitable »,
174
+ `find_lignes_adressage` rend les mêmes lignes sans les départager et laisse remonter la
175
+ `Cpro::NotFoundError`.
176
+
177
+ Une limite à connaître : la résolution ne redescend jamais vers les enfants. Un SIREN nu ne remonte
178
+ rien si la structure n'a d'adresses qu'au niveau SIRET.
166
179
 
167
180
  ## Transmission d'une facture Factur-X
168
181
 
@@ -77,23 +77,25 @@ module Cpro
77
77
  Entities::ResultatRecherche.new(payload, item_class: Entities::CodeRoutage, total_key: TOTAL_KEY)
78
78
  end
79
79
 
80
- # Adresse active la plus proche d'une valeur d'adressage : la valeur elle-même si elle porte
81
- # une plateforme active, sinon son parent le plus proche, et ainsi de suite jusqu'au SIREN.
82
- # Rend la ligne d'annuaire, dont `to_s` donne l'identifiant à porter en BT-49.
80
+ # Adresse à porter en BT-49 pour une valeur d'adressage : la plus proche que l'annuaire
81
+ # connaisse, en privilégiant celles dont la plateforme est active. Rend la ligne d'annuaire,
82
+ # dont `to_s` donne l'identifiant.
83
83
  #
84
- # `nil` a un sens unique aucun niveau n'est adressable, donc on ne peut pas facturer. Pour
85
- # distinguer « SIREN inconnu » de « connu mais sans plateforme active », voir
86
- # `#find_lignes_adressage`, qui ne filtre pas et laisse remonter le 404.
84
+ # Le statut de plateforme départage, il n'exclut pas. Une adresse sans plateforme reste
85
+ # utilisable : le dossier de spécifications externes pose qu'une facture destinée à un
86
+ # destinataire non raccordé prend le statut « Déposée » avec le motif NON_TRANSMISE, et que
87
+ # ce statut « permet à l'entité publique de justifier qu'elle a bien émis une facture
88
+ # électronique et qu'elle a donc respecté ses obligations réglementaires ». L'émetteur devra
89
+ # remettre un duplicata à son client, mais la facture est valide et l'obligation remplie.
90
+ #
91
+ # L'appelant lit `plateforme_active?` sur la ligne rendue pour savoir dans lequel des deux
92
+ # cas il se trouve. `nil` ne signifie plus qu'une chose : aucune adresse, à aucun niveau.
87
93
  #
88
94
  # Un seul appel réseau : `GET /siren/code-insee:` retourne aussi les lignes SIRET et service.
89
- def find_identifiant_actif(valeur)
95
+ def find_identifiant_adressage(valeur)
90
96
  adressage = Adressage.parse(valeur)
91
- actives = find_lignes_adressage(adressage).select(&:plateforme_active?)
92
- adressage.paliers.each do |palier|
93
- ligne = actives.find { |active| palier.correspond?(active) }
94
- return ligne if ligne
95
- end
96
- nil
97
+ lignes = find_lignes_adressage(adressage)
98
+ plus_proche(adressage, lignes.select(&:plateforme_active?)) || plus_proche(adressage, lignes)
97
99
  rescue NotFoundError
98
100
  nil
99
101
  end
@@ -110,6 +112,16 @@ module Cpro
110
112
 
111
113
  private
112
114
 
115
+ # Descend les paliers du plus précis au plus général et rend la première ligne qui
116
+ # corresponde. Appelée deux fois : sur les adresses actives d'abord, sur toutes ensuite.
117
+ def plus_proche(adressage, lignes)
118
+ adressage.paliers.each do |palier|
119
+ ligne = lignes.find { |candidate| palier.correspond?(candidate) }
120
+ return ligne if ligne
121
+ end
122
+ nil
123
+ end
124
+
113
125
  def normalize(value, length, label)
114
126
  digits = value.to_s.gsub(/\s+/, "")
115
127
  return digits if digits.match?(/\A\d{#{length}}\z/)
data/lib/cpro/suivi.rb ADDED
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cpro
4
+ # Photographie du parcours d'un dépôt, à un instant donné. Elle enchaîne les appels que l'hôte
5
+ # devrait sinon écrire lui-même — statut du flux, recherche de la facture, consultation avec son
6
+ # historique — et s'arrête dès qu'un palier ne permet pas d'aller plus loin.
7
+ #
8
+ # Elle n'attend pas : la cadence d'interrogation appartient à l'appelant. Et elle n'avale rien,
9
+ # `statut_flux` et `facture` restant accessibles pour ce qu'elle n'interprète pas.
10
+ class Suivi
11
+ ETAPES = %i[flux_en_attente flux_irrecevable facture_introuvable facture_connue].freeze
12
+
13
+ attr_reader :etape, :statut_flux, :facture
14
+
15
+ # Un 404 sur le flux et une recherche vide sont des états du parcours, pas des erreurs : ils
16
+ # deviennent des étapes. Tout le reste — 401, 403, 5xx — continue de lever.
17
+ def self.pour(client, depot)
18
+ statut = client.flux.status(depot)
19
+ return new(etape: :flux_en_attente) if statut.nil?
20
+ return new(etape: :flux_irrecevable, statut_flux: statut) unless statut.recevable?
21
+
22
+ facture = premiere_facture(client, statut)
23
+ return new(etape: :facture_introuvable, statut_flux: statut) if facture.nil?
24
+
25
+ new(etape: :facture_connue, statut_flux: statut, facture: facture)
26
+ end
27
+
28
+ # `find` seul renseigne l'historique, et c'est lui qui porte les motifs.
29
+ def self.premiere_facture(client, statut)
30
+ return nil if statut.nom_flux.nil? || statut.nom_flux.empty?
31
+
32
+ resultat = client.factures.search_by_nom_flux(statut).first
33
+ resultat && client.factures.find(resultat)
34
+ end
35
+ private_class_method :premiere_facture
36
+
37
+ def initialize(etape:, statut_flux: nil, facture: nil)
38
+ raise ArgumentError, "étape inconnue : #{etape.inspect}" unless ETAPES.include?(etape)
39
+
40
+ @etape = etape
41
+ @statut_flux = statut_flux
42
+ @facture = facture
43
+ end
44
+
45
+ def en_attente?
46
+ etape == :flux_en_attente
47
+ end
48
+
49
+ def irrecevable?
50
+ etape == :flux_irrecevable
51
+ end
52
+
53
+ # Ambigu par nature : l'API répond 204 aussi bien parce que la facture n'est pas encore
54
+ # indexée que parce que le compte technique n'est rattaché à aucune des deux structures.
55
+ def introuvable?
56
+ etape == :facture_introuvable
57
+ end
58
+
59
+ def connue?
60
+ etape == :facture_connue
61
+ end
62
+
63
+ # Vrai que l'incident soit survenu au niveau de l'enveloppe ou à celui de la facture.
64
+ def en_echec?
65
+ irrecevable? || facture&.en_echec? || false
66
+ end
67
+
68
+ # Les motifs vivent à deux endroits selon l'endroit où le dépôt s'est arrêté : l'appelant n'a
69
+ # pas à savoir lequel.
70
+ def motifs
71
+ return statut_flux.motifs_rejet if irrecevable?
72
+
73
+ facture ? facture.motifs_rejet : []
74
+ end
75
+
76
+ # Le dernier changement de statut porteur de motifs. Nil à l'étape `:flux_irrecevable` : il
77
+ # n'y a alors pas de facture, et les motifs sont portés par le flux — `motifs` reste l'accès
78
+ # uniforme, quelle que soit l'étape atteinte.
79
+ def dernier_incident
80
+ facture&.dernier_incident
81
+ end
82
+
83
+ def nom_flux
84
+ statut_flux&.nom_flux
85
+ end
86
+
87
+ def to_s
88
+ [etape, facture&.statut, motifs.map(&:code).join(", ")].reject { |v| v.nil? || v.to_s.empty? }
89
+ .join(" — ")
90
+ end
91
+ end
92
+ end
data/lib/cpro/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Cpro
4
- VERSION = "0.2.5"
4
+ VERSION = "0.2.6"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cpro-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.5
4
+ version: 0.2.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hôtentic
@@ -85,6 +85,7 @@ files:
85
85
  - lib/cpro/resources/base.rb
86
86
  - lib/cpro/resources/factures.rb
87
87
  - lib/cpro/resources/flux.rb
88
+ - lib/cpro/suivi.rb
88
89
  - lib/cpro/version.rb
89
90
  homepage: https://hotentic.com
90
91
  licenses: