cpro-client 0.2.8 → 0.3.0

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: 629795027a48606736ffd0273cd3bf2b1e457237508f428978a4bdbce3245f56
4
- data.tar.gz: 5a3a218220129860479c710a68aaf4eb612ab74f2f6c8a812a7aa65e2f90f7a0
3
+ metadata.gz: 2f8a0732008b70f7a94ea7689d1ecd6675d6a8f6203330ada88e66976baf3d0c
4
+ data.tar.gz: 0fbef1fe26a7ea78cbfe8b5e472dbe5daa6ebb09ce166d00f9133932f7f03c79
5
5
  SHA512:
6
- metadata.gz: 23cc35215eb0530f66b71677d35948c1da2896c626a400018901de87c1e2ed8d517ffcb3eefe5f02d5978a7e1f966c513fd8cb1c114aa3950294fb0805915f21
7
- data.tar.gz: 89a63f46184b02fb02ce615a964148f3ee3997899855312bacd77f3c41d5a7da6cd1b0ee255040fcf2304d3fa05541b13fbb57915ebca9b0c5187b7c07a20b80
6
+ metadata.gz: 555fc82d332aa7a0c17a679fc87f405cffcfb0c97589f0266b01056a20ef53237809c4a17e09149b24121efe045fb6fa8d8b18f0b7753550bf01c74b3e1b75f0
7
+ data.tar.gz: 13b6e6d2fa7a66b87d30fae3e03e9561f6666d6f019e0e3918d1d7dce8e43e6a063042de58e72984aebfc873f62ae36d6d4f243defcde3a82390d3f2f797cb4f
data/CHANGELOG.md CHANGED
@@ -1,6 +1,60 @@
1
1
  ## [Unreleased]
2
2
 
3
3
 
4
+ ## [0.3.0] - 2026-10-12
5
+
6
+ ### Ajouté
7
+ - `Cpro::Resources::Flux#deposit_facturx` confronte l'adresse de l'acheteur (BT-49) à l'annuaire
8
+ avant de déposer, et lève une `Cpro::AdressageError` si elle est absente, hors schemeID `0225`,
9
+ privée de SIREN, ou inconnue. Chorus Pro ne la contrôle qu'au traitement, plusieurs minutes après
10
+ le dépôt : la facture est alors rejetée `ADR_ERR` ou `REJ_ADR`, et son numéro consommé — mesuré
11
+ le 30/08/2026. Le message dit ce qui bloque et que faire ; `motif` permet de brancher, et
12
+ `adresses_connues` liste les adresses de l'annuaire pour régénérer la facture. Toutes les pages de
13
+ lignes sont parcourues, et la comparaison se fait mot pour mot, forme SIREN_SUFFIXE comprise.
14
+
15
+ **Le contrôle est actif par défaut** et coûte un appel à l'Annuaire par dépôt ;
16
+ `verifier_adressage: false` s'en dispense. Une erreur de l'annuaire remonte telle quelle et
17
+ empêche le dépôt.
18
+ - `Cpro::Facturx.adresse_acheteur` lit BT-49 et son schemeID dans le XML embarqué, quel que soit le
19
+ préfixe d'espace de noms, en ignorant l'adresse du vendeur et le courriel du contact acheteur.
20
+
21
+ ### Modifié
22
+ - `Cpro::Resources::Annuaire#find_identifiant_adressage` suit l'ordre que recommande le dossier de
23
+ spécifications externes (§3.1.2) pour adresser un SIRET. Un établissement sans adresse propre
24
+ (`SIREN_SIRET`) est désormais adressé à son premier code routage par ordre alphanumérique
25
+ (`SIREN_SIRET_CodeRoutage`) avant de remonter au SIREN ; la résolution ne faisait jusqu'ici que
26
+ remonter. Seuls les codes routage de l'établissement visé comptent, et un SIREN nu ne descend
27
+ toujours pas vers ses établissements. **L'adresse rendue change** pour un SIRET qui n'a que des
28
+ codes routage.
29
+
30
+ ### Corrigé
31
+ - `Cpro::Resources::Annuaire#find_identifiant_adressage` pouvait rendre une adresse SIREN_SUFFIXE
32
+ au palier SIREN : sans SIRET ni code routage, elle ressemblait à la ligne SIREN nue, et
33
+ l'emportait si l'annuaire la listait en premier. Les adresses à suffixe sortent de la hiérarchie
34
+ SIREN → SIRET → service et ne servent plus qu'en dernier recours, la première par ordre
35
+ alphanumérique, comme le recommande le dossier de spécifications externes (§3.1.2). Une ligne
36
+ est reconnue à suffixe par son champ `suffixeAdressage`, ou à défaut par sa forme
37
+ (`Cpro::Entities::LigneAnnuaire#suffixe?`).
38
+ - `Cpro::Client#suivi` classait en `:flux_irrecevable` tout flux qui n'était pas `RECEVABLE` — y
39
+ compris `ENTRANT_ENVELOPPE_VALIDE`, le statut intermédiaire que déclare le swagger, sans aucun
40
+ motif à l'appui. L'appelant, invité à corriger et redéposer, aurait vu son second dépôt refusé
41
+ en doublon. Seul `IRRECEVABLE` mène désormais à `:flux_irrecevable` ; tout autre statut laisse le
42
+ flux en `:flux_en_attente`, le statut brut restant lisible par `statut_flux`.
43
+
44
+ ### Modifié
45
+ - Le message des erreurs d'API nomme la requête qui a échoué, méthode et chemin sans la requête :
46
+ `400 Unexpected value 'EN_COURS_TRAITEMENT' (GET /g2b/flux/v1/flux/…)`. Relevé en production :
47
+ `suivi` enchaîne trois appels, et l'erreur lue dans les journaux de l'hôte ne disait pas lequel
48
+ avait levé. Un message vide ne laisse plus d'espace final.
49
+
50
+ ### Documentation
51
+ - `:deposee` ne veut plus dire que « l'acheminement n'a pas encore eu lieu », comme l'annonçait la
52
+ 0.2.8. D'après la spécification, une facture `DEPOSEE` sans motif est émise vers la plateforme
53
+ du destinataire, et l'annexe des statuts déclare facultatifs tous ceux qui le confirmeraient —
54
+ `EMISE_PAR_LA_PLATEFORME`, `RECUE_DE_LA_PLATEFORME`, `MISE_A_DISPOSITION`, `PRISE_EN_CHARGE`.
55
+ L'acheminement n'est donc ni prouvé ni exclu. `Suivi#orientation` ne change pas de comportement.
56
+
57
+
4
58
  ## [0.2.8] - 2026-09-25
5
59
 
6
60
  ### Ajouté
@@ -12,11 +66,11 @@
12
66
  - `Cpro::Suivi#orientation` cesse de traiter `:en_cours` comme un fourre-tout, qui absorbait neuf
13
67
  statuts sur vingt-deux — dont `ENCAISSEE`, une facture payée ressortant « en cours ». Deux
14
68
  valeurs s'ajoutent :
15
- - **`:deposee`** — `DEPOSEE` sans aucun motif. Le dépôt a réussi, la plateforme n'a rien à
16
- signaler, l'acheminement n'a pas encore eu lieu ;
17
- - **`:cycle_de_vie`** — `ENCAISSEE`, `COMPLETEE`, `ANNULEE`, `CHANGEMENT_DE_COMPTE_A_PAYER`, les
18
- statuts que seul le vendeur pose. Ils ne prouvent pas l'acheminement : on encaisse une facture
19
- jamais transmise après remise d'un duplicata.
69
+ - **`:deposee`** — `DEPOSEE` sans aucun motif. Le dépôt a réussi, la plateforme n'a rien à
70
+ signaler, l'acheminement n'a pas encore eu lieu ;
71
+ - **`:cycle_de_vie`** — `ENCAISSEE`, `COMPLETEE`, `ANNULEE`, `CHANGEMENT_DE_COMPTE_A_PAYER`, les
72
+ statuts que seul le vendeur pose. Ils ne prouvent pas l'acheminement : on encaisse une facture
73
+ jamais transmise après remise d'un duplicata.
20
74
 
21
75
  `:en_cours` ne garde que `DEMANDE_DE_PAIEMENT_DIRECT` et les trois statuts d'affacturage, sur
22
76
  lesquels nous n'avons ni mesure ni lecture. Un statut posé par le vendeur l'emporte désormais sur
@@ -81,6 +135,12 @@
81
135
  par `Factures#find`.
82
136
 
83
137
 
138
+ ## [0.2.4] - 2026-09-04
139
+
140
+ ### Corrigé
141
+ - Republication de la 0.2.3, dont le paquet publié était incomplet. Aucun changement de code.
142
+
143
+
84
144
  ## [0.2.3] - 2026-08-31
85
145
 
86
146
  ### Ajouté
data/CHORUS_PRO.md CHANGED
@@ -67,7 +67,7 @@ précède.
67
67
  | **Destinataire sans plateforme** *(mesuré 04/09)* | `RECEVABLE` (500) | `DEPOSEE` [`NON_TRANSMISE`] | **conforme** — jamais transmise, duplicata à la charge de l'émetteur |
68
68
  | **Plateforme non raccordée** *(mesuré 04/09)* | `RECEVABLE` (500) | `DEPOSEE` [`NON_TRANSMISE`] | **conforme** — sera transmise au raccordement de la plateforme |
69
69
  | **Doublon** *(mesuré 04/09)* | `RECEVABLE` (500) | `DEPOSEE` [`REJ_UNI`] | le triptyque est déjà pris |
70
- | **Transmise** *(documenté)* | `RECEVABLE` (500) | `DEPOSEE`, puis les statuts que la plateforme et le destinataire posent | nominal — **jamais observé en sandbox**, et l'ordre exact n'est pas documenté |
70
+ | **Transmise** *(documenté)* | `RECEVABLE` (500) | `DEPOSEE` sans motif, puis éventuellement les statuts que la plateforme et le destinataire posent — tous facultatifs | nominal — émise vers la plateforme du destinataire, sans accusé obligatoire ; **acheminement jamais vérifié** |
71
71
  | **Non-conformité** *(documenté)* | `RECEVABLE` (500) | `REJETEE` | la facture n'est pas intégrée ; retransmission après correction possible |
72
72
  | **Routage impossible** *(documenté)* | `RECEVABLE` (500) | `ERREUR_ROUTAGE` (221) | terminal |
73
73
 
@@ -108,7 +108,7 @@ couple **statut × motif**.
108
108
 
109
109
  | Cas | Statut | Motif | Comment le distinguer |
110
110
  | --- | --- | --- | --- |
111
- | **En cours** | `DEPOSEE` | aucun | rien à faire, réinterroger |
111
+ | **Déposée sans incident** | `DEPOSEE` | aucun | émise vers le destinataire d'après la spécification, sans que rien le confirme ; réinterroger |
112
112
  | **Rejet corrigeable** | `REJETEE` | conformité *(documenté)* | la facture n'est pas intégrée : corriger et redéposer sous le même numéro |
113
113
  | **Rejet définitif** | `REJETEE` | `REJ_ADR` *(mesuré)* | la facture est intégrée, son numéro est consommé : avoir + nouvelle facture |
114
114
  | | `ERREUR_ROUTAGE` | — | acheminée mais non routable chez le destinataire |
@@ -125,6 +125,11 @@ couple **statut × motif**.
125
125
  D'après la colonne « Sens flux » de l'annexe, ces statuts proviennent de la plateforme du
126
126
  destinataire ou constatent l'émission par Chorus Pro. Les recevoir prouve que la facture est partie.
127
127
 
128
+ **La réciproque est fausse.** Hormis `REFUSEE` (210), l'annexe les déclare tous **facultatifs** : ni
129
+ Chorus Pro ni la plateforme du destinataire ne sont tenus de les poser. Une facture qui n'en reçoit
130
+ aucun peut très bien être arrivée — la spécification (§3.2.3.3.2) prévoit qu'en cas nominal Chorus
131
+ Pro appose `DEPOSEE` puis émet la facture, sans autre statut.
132
+
128
133
  ```ruby
129
134
  TRANSMISE = %w[
130
135
  EMISE_PAR_LA_PLATEFORME RECUE_DE_LA_PLATEFORME MISE_A_DISPOSITION PRISE_EN_CHARGE
@@ -150,7 +155,7 @@ sont en revanche jamais des preuves d'acheminement.
150
155
  | `:en_attente` | flux non encore jugé | réinterroger |
151
156
  | `:enveloppe_irrecevable` | flux `IRRECEVABLE` | corriger le fichier, redéposer |
152
157
  | `:facture_introuvable` | recherche vide | réinterroger, puis vérifier le rattachement du compte |
153
- | `:deposee` | `DEPOSEE` sans motif | réinterroger — dépôt réussi, acheminement en attente |
158
+ | `:deposee` | `DEPOSEE` sans motif | réinterroger — dépôt réussi, acheminement ni prouvé ni exclu |
154
159
  | `:cycle_de_vie` | statut posé par le vendeur | la facture vit sa vie commerciale |
155
160
  | `:en_cours` | statut non classé | ne rien en déduire |
156
161
  | `:rejetee` | statut `REJETEE` | lire les motifs |
@@ -160,11 +165,11 @@ sont en revanche jamais des preuves d'acheminement.
160
165
 
161
166
  ```ruby
162
167
  case suivi.orientation
163
- when :en_attente, :en_cours, :facture_introuvable then reprogrammer_le_suivi
164
- when :enveloppe_irrecevable, :rejetee then signaler(suivi.motifs)
165
- when :non_transmise then preparer_un_duplicata
166
- when :erreur_routage then alerter
167
- when :transmise then classer
168
+ when :en_attente, :facture_introuvable, :deposee, :en_cours then reprogrammer_le_suivi
169
+ when :enveloppe_irrecevable, :rejetee then signaler(suivi.motifs)
170
+ when :non_transmise then preparer_un_duplicata
171
+ when :erreur_routage then alerter
172
+ when :transmise, :cycle_de_vie then classer
168
173
  end
169
174
  ```
170
175
 
@@ -205,7 +210,8 @@ pris, ce qui se traite indépendamment de l'orientation.
205
210
  **Ce qui est mesuré et ce qui ne l'est pas.** Seuls `REJETEE`/`REJ_ADR`, `DEPOSEE`/`NON_TRANSMISE`
206
211
  dans ses deux variantes et `DEPOSEE`/`REJ_UNI` ont été observés. Le rejet de conformité,
207
212
  `ERREUR_ROUTAGE` et l'ensemble des statuts d'acheminement sont déduits de l'annexe et de la
208
- spécification, jamais vus en sandbox.
213
+ spécification, jamais vus en sandbox. `DEPOSEE` sans motif a été relevé en production, sans que
214
+ l'on sache encore si les factures concernées sont parvenues à leur destinataire.
209
215
 
210
216
  ## Motifs rencontrés
211
217
 
@@ -260,6 +266,12 @@ que le `nomFlux`, dont le format doit changer.
260
266
 
261
267
  **Allotir.** La spécification recommande un maximum de 500 factures par flux.
262
268
 
269
+ **BT-23 ne dépend pas du circuit.** La lettre dit ce qui est facturé, pas par quel circuit :
270
+ le swagger Recherche Factures énumère `B1 S1 M1 B2 S2 M2 B4 S4 M4 S5 S6 B7 S7` pour
271
+ `typeProcessusMetier` — **B**iens, **S**ervices, **M**ixte, déclinés par numéro de processus. `S1`
272
+ est donc valide en G2B, et l'exemple d'AIFE porte `B1` parce que sa facture de test concerne des
273
+ biens.
274
+
263
275
  **Un destinataire public ne se facture pas en G2B.** Relevé en production le 25/09 :
264
276
 
265
277
  > `ROUTAGE_ERR` — « Il n'est pas possible de transmettre des factures intra sphère publique par ce
@@ -273,15 +285,18 @@ qui vaut `Publique` ou `Privée assujettie` — et seule la **recherche** la ser
273
285
  laissant le champ nul.
274
286
 
275
287
  **Un rejet n'est pas une étape plus avancée qu'un dépôt.** `REJETEE` et `ERREUR_ROUTAGE` prouvent
276
- que la plateforme a traité la facture, jamais qu'elle l'a transmise. Pour mesurer si un circuit
277
- fonctionne, ne comptez que les statuts de `Suivi::STATUTS_TRANSMISE` — tout le reste, y compris les
278
- échecs, laisse la question ouverte.
288
+ que la plateforme a traité la facture, jamais qu'elle l'a transmise. Seuls les statuts de
289
+ `Suivi::STATUTS_TRANSMISE` prouvent l'acheminement, mais ils sont facultatifs : les compter donne un
290
+ minimum, pas une mesure du circuit. Leur absence ne permet de rien conclure.
279
291
 
280
292
  **Une facture peut rester `DEPOSEE` indéfiniment, sans aucun motif.** Relevé en production : une
281
293
  facture déposée le 18/09 était encore `DEPOSEE` avec un `detailStatut` vide une semaine plus tard.
282
- Chorus Pro n'a alors rien à dire, et aucune API ne donne de raison. La seule piste est l'annuaire :
283
- si le destinataire n'a pas de plateforme active, la facture n'ira nulle part — même sans motif
284
- `NON_TRANSMISE` pour le signaler. `local/sonde_bloquees.rb` automatise ce recoupement.
294
+ C'est ce que la spécification décrit pour le cas nominal — `DEPOSEE`, puis émission vers la
295
+ plateforme du destinataire, les statuts suivants étant facultatifs. Ce n'est donc pas en soi le
296
+ signe d'un blocage, mais aucune API ne permet de vérifier l'arrivée : seul le destinataire le peut.
297
+ L'annuaire sert à écarter les autres cas : face à un destinataire sans plateforme active, la
298
+ spécification prévoit le motif `NON_TRANSMISE`, et son absence serait une anomalie.
299
+ `local/sonde_bloquees.rb` automatise ce recoupement.
285
300
 
286
301
  **Une requête hors bornes donne un 500, pas un 400.** `limite` est plafonné à 50 sur
287
302
  `POST /facture/recherche` ; au-delà, la plateforme répond `500 Erreur interne` sans rien expliquer.
@@ -289,6 +304,18 @@ Le même comportement a été relevé sur l'Annuaire, où un filtre `siret` priv
289
304
  donne également un 500. **Ne lisez pas un 500 comme une panne avant d'avoir vérifié votre requête
290
305
  contre le swagger.** `Factures#search` refuse désormais localement une pagination hors bornes.
291
306
 
307
+ **L'Annuaire G2B peut ignorer une entreprise que l'annuaire en ligne connaît.** Relevé en
308
+ production le 12/10 : pour une SAS active et diffusible, dont l'annuaire en ligne de Chorus Pro
309
+ affiche une adresse, `GET /siren/code-insee:` et `GET /siret/code-insee:` répondent tous deux 404
310
+ — l'unité légale entière est absente de la vue G2B, pas seulement l'établissement. L'API Super PDP
311
+ voit au même moment une seule ligne, au niveau SIREN, inactive. L'explication la plus probable,
312
+ non confirmée : l'API ne rend que « les lignes courantes et celles étant en attente d'activation
313
+ sous un délai de 7 jours » (Annexe API V6.1, §3.2.1), et cette ligne n'entre pas encore dans cette
314
+ fenêtre. `find_identifiant_adressage` rend alors `nil`, et le contrôle d'adresse de
315
+ `deposit_facturx` bloque en `:siren_inconnu` — c'est voulu : Chorus Pro contrôlera l'adresse au
316
+ traitement. Une facture générée sur le SIREN de l'acheteur pourra partir quand la ligne entrera
317
+ dans la vue G2B.
318
+
292
319
  **Le 204 de Recherche Factures est ambigu** : aucune facture correspondante, ou compte technique
293
320
  rattaché ni à la structure émettrice ni à la destinataire. Rien ne permet de les distinguer.
294
321
 
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- cpro-client (0.2.9)
4
+ cpro-client (0.3.0)
5
5
  faraday (>= 0.9, < 3.0)
6
6
 
7
7
  GEM
data/README.md CHANGED
@@ -140,11 +140,22 @@ Filtrez sur `siret`, faute de quoi la réponse balaie tout l'annuaire.
140
140
 
141
141
  BT-49, l'adresse électronique de l'acheteur, ne se transmet pas à l'API : elle voyage dans le XML
142
142
  Factur-X, et c'est d'elle que le PPF déduit l'acheminement. Une facture qui la porte mal est
143
- rejetée `REJ_ADR` — « le destinataire n'est pas trouvable » — plusieurs minutes après le dépôt.
143
+ rejetée `REJ_ADR` — « le destinataire n'est pas trouvable » — plusieurs minutes après le dépôt, et
144
+ son numéro est consommé. `deposit_facturx` le vérifie donc avant d'envoyer quoi que ce soit (voir
145
+ [plus bas](#contrôle-de-ladresse-avant-dépôt)).
144
146
 
145
147
  `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
+ valeur d'adressage — SIREN, SIRET, SIRET_SERVICE, SIREN_SIRET_SERVICE — et rend l'adresse la plus
149
+ proche que l'annuaire connaisse, dans l'ordre que recommande le dossier de spécifications externes
150
+ (§3.1.2) pour adresser un SIRET :
151
+
152
+ | Ordre | Adresse retenue |
153
+ | :-: | --- |
154
+ | 1 | le service demandé, si la valeur en porte un : `SIREN_SIRET_SERVICE` |
155
+ | 2 | l'adresse propre de l'établissement : `SIREN_SIRET` |
156
+ | 3 | à défaut, son premier code routage par ordre alphanumérique : `SIREN_SIRET_CodeRoutage` |
157
+ | 4 | à défaut, l'entreprise : `SIREN` |
158
+ | 5 | à défaut, la première adresse à suffixe par ordre alphanumérique : `SIREN_Suffixe` |
148
159
 
149
160
  ```ruby
150
161
  ligne = client.annuaire.find_identifiant_adressage("71915767420316")
@@ -155,8 +166,13 @@ client.annuaire.find_identifiant_adressage("71915767420316_SERVICE_INEXISTANT").
155
166
  # => "719157674_71915767420316" — service inconnu, on retombe sur son parent
156
167
  ```
157
168
 
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
169
+ Les codes routage pris en compte à l'étape 3 sont ceux de l'établissement visé, jamais ceux d'un
170
+ autre établissement de la même entreprise. Un SIREN nu, lui, ne descend pas vers ses
171
+ établissements : il ne rend rien si l'entreprise n'a d'adresses qu'au niveau SIRET.
172
+
173
+ **Le statut de plateforme départage, il n'exclut pas.** Une adresse dont la plateforme est active
174
+ l'emporte, même plus éloignée dans l'ordre ci-dessus ; mais si aucune ne l'est, l'adresse la plus
175
+ proche est rendue quand même. Elle
160
176
  reste la bonne : une facture destinée à un non-raccordé est bien *déposée*, avec le motif
161
177
  `NON_TRANSMISE`, et ce statut « permet à l'entité publique de justifier qu'elle a bien émis une
162
178
  facture électronique et qu'elle a donc respecté ses obligations réglementaires ». L'émetteur devra
@@ -174,9 +190,6 @@ Pour distinguer « unité légale inconnue » de « connue mais sans adresse exp
174
190
  `find_lignes_adressage` rend les mêmes lignes sans les départager et laisse remonter la
175
191
  `Cpro::NotFoundError`.
176
192
 
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.
179
-
180
193
  ## Transmission d'une facture Factur-X
181
194
 
182
195
  ```ruby
@@ -198,6 +211,58 @@ attend :
198
211
  Cpro::Facturx.montant_total(File.binread("facture.pdf")) # => BigDecimal("1910.40")
199
212
  ```
200
213
 
214
+ ### Contrôle de l'adresse avant dépôt
215
+
216
+ Chorus Pro ne contrôle l'adresse de l'acheteur (BT-49) qu'au traitement, plusieurs minutes après le
217
+ dépôt. Absente, la facture est rejetée `ADR_ERR` ; inconnue de l'annuaire, `REJ_ADR`. Dans les deux
218
+ cas son **numéro est consommé** : la redéposer sous le même numéro échoue, il faut un avoir et une
219
+ nouvelle facture.
220
+
221
+ `deposit_facturx` lit donc BT-49 dans le XML embarqué et le confronte à l'annuaire avant de
222
+ déposer, au prix d'un appel. La comparaison se fait mot pour mot sur toutes les lignes de l'unité
223
+ légale, forme SIREN_SUFFIXE comprise. Si l'adresse ne passe pas, il lève une
224
+ `Cpro::AdressageError` — rien n'est parti, le numéro reste disponible :
225
+
226
+ ```ruby
227
+ begin
228
+ client.flux.deposit_facturx("facture.pdf")
229
+ rescue Cpro::AdressageError => e
230
+ e.message # "Dépôt bloqué : l'adresse électronique de l'acheteur (BT-49) « 474775418 »
231
+ # est inconnue de l'annuaire. Adresses connues pour le SIREN 474775418 : …"
232
+ e.motif # :adresse_inconnue
233
+ e.identifiant # "474775418" — l'adresse portée par la facture
234
+ e.adresses_connues # ["474775418_47477541867167"] — de quoi régénérer la facture
235
+ end
236
+ ```
237
+
238
+ | `motif` | Cause | Suite |
239
+ | --- | --- | --- |
240
+ | `:adresse_absente` | la facture ne porte pas de BT-49 | régénérer la facture |
241
+ | `:schema_invalide` | BT-49 n'est pas en schemeID `0225` (BR-FR-21) | régénérer la facture |
242
+ | `:siren_absent` | BT-49 ne commence pas par un SIREN (BR-FR-21) | régénérer la facture |
243
+ | `:siren_inconnu` | l'annuaire ne connaît pas ce SIREN | vérifier le SIREN, ou réessayer plus tard |
244
+ | `:adresse_inconnue` | le SIREN est connu, pas cette adresse | régénérer avec `adresses_connues`, ou réessayer plus tard |
245
+
246
+ Les deux derniers motifs peuvent se résoudre d'eux-mêmes, quand l'acheteur déclare son adresse :
247
+ c'est ce qui permet de **générer une facture sur une adresse présumée** — le SIREN de l'acheteur,
248
+ faute de mieux — et de retenter le dépôt à intervalle régulier sans jamais risquer son numéro.
249
+ Une erreur de l'annuaire lui-même (401, 403, 5xx) remonte telle quelle : faute de pouvoir
250
+ vérifier, on ne dépose pas.
251
+
252
+ `verifier_adressage: false` dispense du contrôle, pour qui l'a déjà fait :
253
+
254
+ ```ruby
255
+ client.flux.deposit_facturx("facture.pdf", verifier_adressage: false)
256
+ ```
257
+
258
+ L'adresse se lit aussi hors dépôt :
259
+
260
+ ```ruby
261
+ adresse = Cpro::Facturx.adresse_acheteur(File.binread("facture.pdf")) # nil si la facture n'en porte pas
262
+ adresse.identifiant # => "474775418"
263
+ adresse.schema # => "0225"
264
+ ```
265
+
201
266
  Le même endpoint accepte trois autres types de flux, qui ne diffèrent que par leur code interface :
202
267
 
203
268
  ```ruby
@@ -288,11 +353,11 @@ Pour brancher directement sans reconstruire le même `if` dans chaque applicatio
288
353
 
289
354
  ```ruby
290
355
  case suivi.orientation
291
- when :en_attente, :en_cours, :facture_introuvable then reprogrammer_le_suivi
292
- when :enveloppe_irrecevable, :rejetee then signaler(suivi.motifs)
293
- when :non_transmise then preparer_un_duplicata
294
- when :erreur_routage then alerter
295
- when :transmise then classer
356
+ when :en_attente, :facture_introuvable, :deposee, :en_cours then reprogrammer_le_suivi
357
+ when :enveloppe_irrecevable, :rejetee then signaler(suivi.motifs)
358
+ when :non_transmise then preparer_un_duplicata
359
+ when :erreur_routage then alerter
360
+ when :transmise, :cycle_de_vie then classer
296
361
  end
297
362
  ```
298
363
 
@@ -318,8 +383,8 @@ facture — `motifs` reste alors l'accès uniforme.
318
383
 
319
384
  | Étape | Ce qui s'est passé | Appels | Suite |
320
385
  | --- | --- | :-: | --- |
321
- | `:flux_en_attente` | 404 sur le flux — cas nominal | 1 | réinterroger |
322
- | `:flux_irrecevable` | enveloppe refusée, motifs sur le flux | 1 | terminal |
386
+ | `:flux_en_attente` | 404 sur le flux — cas nominal —, ou statut autre que `RECEVABLE` et `IRRECEVABLE` | 1 | réinterroger |
387
+ | `:flux_irrecevable` | statut `IRRECEVABLE`, motifs sur le flux | 1 | terminal |
323
388
  | `:facture_introuvable` | flux recevable, recherche vide | 2 | réinterroger, puis vérifier le rattachement du compte |
324
389
  | `:facture_connue` | facture et historique disponibles | 3 | lire `en_echec?` et les motifs |
325
390
 
@@ -327,6 +392,11 @@ La méthode **n'attend pas** : c'est une photographie, la cadence d'interrogatio
327
392
  Seuls le 404 du flux et la recherche vide deviennent des étapes ; un 401, un 403 ou un 5xx
328
393
  continuent de lever.
329
394
 
395
+ Seul `IRRECEVABLE` vaut verdict d'échec. Le statut intermédiaire `ENTRANT_ENVELOPPE_VALIDE`, que
396
+ déclare le swagger, et toute valeur que la plateforme ajouterait laissent le flux en attente :
397
+ conclure au rejet inviterait à redéposer, et ce second dépôt serait refusé en doublon. Le statut
398
+ brut reste lisible par `suivi.statut_flux`.
399
+
330
400
  ## Servir plusieurs entités
331
401
 
332
402
  L'AIFE distingue trois types de raccordement API. Celui qui s'applique détermine le modèle à
@@ -370,6 +440,7 @@ Toutes les erreurs dérivent de `Cpro::Error`.
370
440
  | Exception | Déclenchement |
371
441
  | --- | --- |
372
442
  | `Cpro::ConfigurationError` | configuration incomplète, compte technique manquant |
443
+ | `Cpro::AdressageError` | dépôt bloqué avant envoi : adresse de l'acheteur absente ou inconnue de l'annuaire |
373
444
  | `Cpro::RequestError` | 400, 406, 415, 422 |
374
445
  | `Cpro::AuthenticationError` | 401, échec d'obtention du jeton |
375
446
  | `Cpro::AuthorizationError` | 403 — habilitations insuffisantes |
@@ -378,7 +449,12 @@ Toutes les erreurs dérivent de `Cpro::Error`.
378
449
  | `Cpro::ServerError` | 5xx |
379
450
 
380
451
  Les erreurs d'API exposent `#status`, `#body` et `#correlation_id` — ce dernier est l'identifiant
381
- PISTE à fournir au support en cas d'incident.
452
+ PISTE à fournir au support en cas d'incident. Leur message nomme la requête qui a échoué, pour
453
+ qu'une erreur lue dans un journal se situe d'elle-même :
454
+
455
+ ```ruby
456
+ error.message # => "400 Unexpected value 'EN_COURS_TRAITEMENT' (GET /g2b/flux/v1/flux/b18e3b6c-…)"
457
+ ```
382
458
 
383
459
  ## Développement
384
460
 
@@ -57,12 +57,20 @@ module Cpro
57
57
  ].compact
58
58
  end
59
59
 
60
- # Une ligne d'annuaire correspond à ce palier si elle décrit le même niveau de précision.
60
+ # Une ligne d'annuaire correspond à ce palier si elle décrit le même niveau de précision. Une
61
+ # adresse à suffixe n'appartient à aucun palier : sans SIRET ni code routage, elle passerait
62
+ # sinon pour la ligne SIREN nue.
61
63
  def correspond?(ligne)
62
- ligne.siret == siret && ligne.identifiant_routage.to_s.empty? == service.nil? &&
64
+ !ligne.suffixe? && ligne.siret == siret && ligne.identifiant_routage.to_s.empty? == service.nil? &&
63
65
  (service.nil? || ligne.identifiant_routage == service)
64
66
  end
65
67
 
68
+ # Une ligne de service de ce SIRET : le palier SIRET y descend faute d'adresse propre.
69
+ def service_de?(ligne)
70
+ !siret.nil? && service.nil? && !ligne.suffixe? && ligne.siret == siret &&
71
+ !ligne.identifiant_routage.to_s.empty?
72
+ end
73
+
66
74
  def to_s
67
75
  [siret ? "#{siren}_#{siret}" : siren, service].compact.join("_")
68
76
  end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cpro
4
+ # Confronte l'adresse de l'acheteur (BT-49) d'un Factur-X à l'annuaire, avant le dépôt.
5
+ #
6
+ # Chorus Pro ne la contrôle qu'au traitement, plusieurs minutes après le dépôt : absente, la
7
+ # facture est rejetée ADR_ERR ; inconnue de l'annuaire, REJ_ADR. Et son numéro est alors
8
+ # consommé — la redéposer sous le même numéro échoue (mesuré le 30/08/2026), il faut un avoir
9
+ # et une nouvelle facture. Un appel à l'annuaire avant le dépôt évite d'en arriver là.
10
+ #
11
+ # La comparaison se fait mot pour mot sur toutes les lignes de l'unité légale, et non par
12
+ # `Annuaire#find_identifiant_adressage` : la forme SIREN_SUFFIXE n'entre dans aucune de celles
13
+ # que lit `Adressage`, et la question n'est pas de trouver l'adresse la plus proche, mais de
14
+ # savoir si celle de la facture existe.
15
+ class ControleAdressage
16
+ SCHEMA_ANNUAIRE = "0225"
17
+ PREFIXE_SIREN = /\A\d{9}/.freeze
18
+ ADRESSES_CITEES = 10
19
+
20
+ CONSEQUENCE = "Chorus Pro rejetterait la facture et son numéro serait consommé."
21
+
22
+ def self.verifier!(annuaire, contenu)
23
+ new(annuaire).verifier!(contenu)
24
+ end
25
+
26
+ def initialize(annuaire)
27
+ @annuaire = annuaire
28
+ end
29
+
30
+ # Rend la ligne d'annuaire de l'adresse, ou lève une AdressageError sans rien avoir déposé.
31
+ def verifier!(contenu)
32
+ adresse = adresse_annuaire(contenu)
33
+ siren = adresse.identifiant[PREFIXE_SIREN]
34
+ lignes = lignes_de(siren) or bloquer(:siren_inconnu, message_siren_inconnu(adresse, siren), adresse)
35
+
36
+ lignes.find { |ligne| ligne.identifiant_adressage == adresse.identifiant } ||
37
+ bloquer(:adresse_inconnue, message_adresse_inconnue(adresse, siren, lignes), adresse, lignes.map(&:to_s))
38
+ end
39
+
40
+ private
41
+
42
+ attr_reader :annuaire
43
+
44
+ # L'adresse de la facture, pourvu qu'elle relève de l'annuaire : présente, en schemeID 0225, et
45
+ # ouverte par un SIREN. Ces trois défauts se voient sans appel réseau.
46
+ def adresse_annuaire(contenu)
47
+ adresse = Facturx.adresse_acheteur(contenu) or bloquer(:adresse_absente, message_absente)
48
+ bloquer(:schema_invalide, message_schema(adresse), adresse) unless adresse.schema == SCHEMA_ANNUAIRE
49
+ bloquer(:siren_absent, message_sans_siren(adresse), adresse) unless adresse.identifiant.match?(PREFIXE_SIREN)
50
+ adresse
51
+ end
52
+
53
+ # Toutes les pages : une grande entreprise porte plus de lignes qu'une page n'en rend. Nil si
54
+ # le SIREN est inconnu de l'annuaire.
55
+ def lignes_de(siren)
56
+ lignes = []
57
+ loop do
58
+ page = annuaire.find_by_siren(siren, **(lignes.empty? ? {} : { ignorer: lignes.size })).lignes_annuaire
59
+ lignes.concat(page.to_a)
60
+ return lignes if page.empty? || lignes.size >= page.nombre_total.to_i
61
+ end
62
+ rescue NotFoundError
63
+ nil
64
+ end
65
+
66
+ def bloquer(motif, message, adresse = nil, adresses_connues = [])
67
+ raise AdressageError.new("Dépôt bloqué : #{message}", motif: motif, identifiant: adresse&.identifiant,
68
+ adresses_connues: adresses_connues)
69
+ end
70
+
71
+ def message_absente
72
+ "la facture ne porte pas d'adresse électronique d'acheteur (BT-49). Chorus Pro la rejetterait " \
73
+ "(ADR_ERR) et son numéro serait consommé. Régénérez-la avec l'adresse de l'acheteur relevée dans " \
74
+ "l'annuaire."
75
+ end
76
+
77
+ def message_schema(adresse)
78
+ "l'adresse électronique de l'acheteur (BT-49) « #{adresse} » est déclarée avec le schemeID " \
79
+ "« #{adresse.schema} », alors que le circuit G2B exige #{SCHEMA_ANNUAIRE}, celui de l'annuaire de la " \
80
+ "facturation électronique (règle BR-FR-21). #{CONSEQUENCE}"
81
+ end
82
+
83
+ def message_sans_siren(adresse)
84
+ "l'adresse électronique de l'acheteur (BT-49) « #{adresse} » ne commence pas par un SIREN, comme " \
85
+ "l'exige la règle BR-FR-21. #{CONSEQUENCE}"
86
+ end
87
+
88
+ def message_siren_inconnu(adresse, siren)
89
+ "le SIREN #{siren}, qui ouvre l'adresse électronique de l'acheteur (BT-49) « #{adresse} », est " \
90
+ "inconnu de l'annuaire de la facturation électronique. Chorus Pro rejetterait la facture (REJ_ADR) " \
91
+ "et son numéro serait consommé. Vérifiez le SIREN de l'acheteur. S'il est juste, son adresse n'est " \
92
+ "peut-être pas encore en vigueur — l'API ne rend que les lignes courantes ou activées sous 7 jours — : " \
93
+ "réessayez plus tard. Si le blocage persiste, l'acheteur n'est pas inscrit, ou n'est pas soumis à la " \
94
+ "facturation électronique."
95
+ end
96
+
97
+ def message_adresse_inconnue(adresse, siren, lignes)
98
+ "l'adresse électronique de l'acheteur (BT-49) « #{adresse} » est inconnue de l'annuaire. " \
99
+ "#{adresses_connues(siren, lignes)} Chorus Pro rejetterait la facture (REJ_ADR) et son numéro serait " \
100
+ "consommé : #{remede(lignes)}."
101
+ end
102
+
103
+ def adresses_connues(siren, lignes)
104
+ return "Aucune adresse n'est connue pour le SIREN #{siren}." if lignes.empty?
105
+
106
+ citees = lignes.first(ADRESSES_CITEES).join(", ")
107
+ suite = lignes.size > ADRESSES_CITEES ? ", … (#{lignes.size} au total)" : ""
108
+ "Adresses connues pour le SIREN #{siren} : #{citees}#{suite}."
109
+ end
110
+
111
+ def remede(lignes)
112
+ return "réessayez quand l'acheteur aura déclaré son adresse" if lignes.empty?
113
+
114
+ "régénérez-la avec l'une de ces adresses, ou réessayez plus tard si l'acheteur déclare la sienne"
115
+ end
116
+ end
117
+ end
@@ -5,6 +5,7 @@ module Cpro
5
5
  class LigneAnnuaire < Base
6
6
  PLATEFORME_ACTIVE = "Plateforme active"
7
7
  PLATEFORME_EN_ATTENTE = "Plateforme en attente d'activation"
8
+ SIREN_NU = /\A\d{9}\z/.freeze
8
9
 
9
10
  def identifiant_adressage
10
11
  payload["identifiantAdressage"]
@@ -38,6 +39,15 @@ module Cpro
38
39
  statut_plateforme == PLATEFORME_EN_ATTENTE
39
40
  end
40
41
 
42
+ # Une adresse SIREN_SUFFIXE, hors de la hiérarchie SIREN → SIRET → service. Reconnue à son
43
+ # champ, ou à défaut à sa forme — sans SIRET, et autre chose qu'un SIREN nu — le champ
44
+ # n'ayant jamais été observé renseigné.
45
+ def suffixe?
46
+ return true unless suffixe_adressage.to_s.empty?
47
+
48
+ siret.to_s.empty? && !identifiant_adressage.to_s.match?(SIREN_NU)
49
+ end
50
+
41
51
  # L'identifiant d'adressage est ce qu'on porte en BT-49 : c'est la valeur utile de la ligne.
42
52
  def to_s
43
53
  identifiant_adressage.to_s
data/lib/cpro/errors.rb CHANGED
@@ -5,6 +5,22 @@ module Cpro
5
5
 
6
6
  class ConfigurationError < Error; end
7
7
 
8
+ # Levée avant un dépôt dont l'adresse de l'acheteur (BT-49) ferait rejeter la facture. Rien n'est
9
+ # parti : le numéro de facture reste disponible.
10
+ #
11
+ # `motif` se prête au branchement : `:adresse_inconnue` et `:siren_inconnu` peuvent se résoudre
12
+ # d'eux-mêmes quand l'acheteur déclare son adresse, les autres demandent de régénérer la facture.
13
+ class AdressageError < Error
14
+ attr_reader :motif, :identifiant, :adresses_connues
15
+
16
+ def initialize(message, motif:, identifiant: nil, adresses_connues: [])
17
+ @motif = motif
18
+ @identifiant = identifiant
19
+ @adresses_connues = adresses_connues
20
+ super(message)
21
+ end
22
+ end
23
+
8
24
  class ApiError < Error
9
25
  attr_reader :status, :body, :correlation_id
10
26
 
data/lib/cpro/facturx.rb CHANGED
@@ -10,6 +10,21 @@ module Cpro
10
10
  ENTETE_PDF = "%PDF"
11
11
  RACINE_CII = "CrossIndustryInvoice"
12
12
 
13
+ # Le bloc de l'acheteur est isolé d'abord : le vendeur porte la même balise (BT-34), et le
14
+ # contact de l'acheteur un courriel sous une balise voisine. Le préfixe d'espace de noms est
15
+ # libre en XML, d'où `(?:\w+:)?`.
16
+ BLOC_ACHETEUR = %r{<(?:\w+:)?BuyerTradeParty\b[^>]*>(.*?)</(?:\w+:)?BuyerTradeParty>}m.freeze
17
+ ADRESSE_ELECTRONIQUE =
18
+ %r{<(?:\w+:)?URIUniversalCommunication>\s*<(?:\w+:)?URIID\b([^>]*)>([^<]*)</(?:\w+:)?URIID>}m.freeze
19
+ SCHEMA = /schemeID\s*=\s*["']([^"']*)["']/.freeze
20
+
21
+ # BT-49 et son schéma, BT-49-1.
22
+ AdresseElectronique = Struct.new(:identifiant, :schema) do
23
+ def to_s
24
+ identifiant
25
+ end
26
+ end
27
+
13
28
  module_function
14
29
 
15
30
  def pdf?(contenu)
@@ -44,6 +59,16 @@ module Cpro
44
59
  nil
45
60
  end
46
61
 
62
+ # Adresse électronique de l'acheteur (BT-49), ou nil si le XML embarqué n'en porte pas.
63
+ def adresse_acheteur(contenu)
64
+ document = xml(contenu) or return nil
65
+ acheteur = document[BLOC_ACHETEUR, 1] or return nil
66
+ attributs, valeur = acheteur.match(ADRESSE_ELECTRONIQUE)&.captures
67
+ return nil if valeur.nil? || valeur.strip.empty?
68
+
69
+ AdresseElectronique.new(valeur.strip, attributs[SCHEMA, 1])
70
+ end
71
+
47
72
  def inflate(flux)
48
73
  Zlib::Inflate.inflate(flux)
49
74
  rescue Zlib::Error
@@ -43,7 +43,15 @@ module Cpro
43
43
  # `reason_phrase` n'existe pas avant Faraday 1.0.
44
44
  def message_for(env)
45
45
  detail = detail_for(env.body) || (env.reason_phrase if env.respond_to?(:reason_phrase))
46
- [env.status, detail].compact.join(" ")
46
+ detail = nil if detail.to_s.strip.empty?
47
+ [env.status, detail, requete(env)].compact.join(" ")
48
+ end
49
+
50
+ # Lue dans les journaux de l'hôte, sans pile d'appels, l'erreur doit dire quel appel a
51
+ # échoué : `Client#suivi`, par exemple, en enchaîne trois. Le chemin suffit à le situer ; la
52
+ # requête en est écartée pour garder un message stable d'un appel à l'autre.
53
+ def requete(env)
54
+ "(#{env.method.to_s.upcase} #{env.url.path})" if env.url
47
55
  end
48
56
 
49
57
  def detail_for(body)
@@ -91,6 +91,8 @@ module Cpro
91
91
  # L'appelant lit `plateforme_active?` sur la ligne rendue pour savoir dans lequel des deux
92
92
  # cas il se trouve. `nil` ne signifie plus qu'une chose : aucune adresse, à aucun niveau.
93
93
  #
94
+ # L'ordre est celui du dossier de spécifications externes (§3.1.2), détaillé sur `plus_proche`.
95
+ #
94
96
  # Un seul appel réseau : `GET /siren/code-insee:` retourne aussi les lignes SIRET et service.
95
97
  def find_identifiant_adressage(valeur)
96
98
  adressage = Adressage.parse(valeur)
@@ -112,14 +114,21 @@ module Cpro
112
114
 
113
115
  private
114
116
 
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
+ # Parcourt les paliers du plus précis au plus général, dans l'ordre que recommande le dossier
118
+ # de spécifications externes (§3.1.2) pour adresser un SIRET : son adresse propre, sinon son
119
+ # premier code routage par ordre alphanumérique, sinon le SIREN, sinon la première adresse à
120
+ # suffixe. Appelée deux fois : sur les adresses actives d'abord, sur toutes ensuite.
117
121
  def plus_proche(adressage, lignes)
118
122
  adressage.paliers.each do |palier|
119
- ligne = lignes.find { |candidate| palier.correspond?(candidate) }
123
+ ligne = lignes.find { |candidate| palier.correspond?(candidate) } ||
124
+ premiere(lignes.select { |candidate| palier.service_de?(candidate) })
120
125
  return ligne if ligne
121
126
  end
122
- nil
127
+ premiere(lignes.select(&:suffixe?))
128
+ end
129
+
130
+ def premiere(lignes)
131
+ lignes.min_by(&:identifiant_adressage)
123
132
  end
124
133
 
125
134
  def normalize(value, length, label)
@@ -3,8 +3,14 @@
3
3
  module Cpro
4
4
  module Resources
5
5
  class Flux < Base
6
- def deposit_facturx(source, name: nil)
7
- depot(source, :facturx, name: name) { |contenu| verifier_facturx(contenu) }
6
+ # Avant de déposer, confronte l'adresse de l'acheteur (BT-49) à l'annuaire, au prix d'un appel :
7
+ # une adresse absente ou inconnue lève une AdressageError au lieu de faire rejeter la facture,
8
+ # et consommer son numéro, plusieurs minutes plus tard. `verifier_adressage: false` s'en passe.
9
+ def deposit_facturx(source, name: nil, verifier_adressage: true)
10
+ depot(source, :facturx, name: name) do |contenu|
11
+ verifier_facturx(contenu)
12
+ ControleAdressage.verifier!(client.annuaire, contenu) if verifier_adressage
13
+ end
8
14
  end
9
15
 
10
16
  def deposit_ubl(source, name: nil)
data/lib/cpro/suivi.rb CHANGED
@@ -17,6 +17,9 @@ module Cpro
17
17
  # de l'annexe des statuts, ils proviennent de la plateforme du destinataire ou constatent
18
18
  # l'émission par la plateforme.
19
19
  #
20
+ # La réciproque est fausse. Hormis `REFUSEE`, l'annexe les déclare tous facultatifs : leur
21
+ # absence ne prouve pas que la facture n'est pas partie.
22
+ #
20
23
  # `APPROUVEE`, `REFUSEE` et `VISEE` circulent dans les deux sens. Ils figurent ici parce que
21
24
  # les recevoir est le cas courant, et que les omettre ferait passer une facture refusée par
22
25
  # l'acheteur pour une facture encore en cours. Une application qui les émet elle-même le sait.
@@ -38,10 +41,16 @@ module Cpro
38
41
 
39
42
  # Un 404 sur le flux et une recherche vide sont des états du parcours, pas des erreurs : ils
40
43
  # deviennent des étapes. Tout le reste — 401, 403, 5xx — continue de lever.
44
+ #
45
+ # Seul IRRECEVABLE est un verdict d'échec. Tout autre statut que RECEVABLE — l'intermédiaire
46
+ # ENTRANT_ENVELOPPE_VALIDE du swagger, ou une valeur que la plateforme ajouterait — laisse le
47
+ # flux en attente : conclure au rejet inviterait à redéposer, et le second dépôt serait refusé
48
+ # en doublon.
41
49
  def self.pour(client, depot)
42
50
  statut = client.flux.status(depot)
43
51
  return new(etape: :flux_en_attente) if statut.nil?
44
- return new(etape: :flux_irrecevable, statut_flux: statut) unless statut.recevable?
52
+ return new(etape: :flux_irrecevable, statut_flux: statut) if statut.irrecevable?
53
+ return new(etape: :flux_en_attente, statut_flux: statut) unless statut.recevable?
45
54
 
46
55
  facture = premiere_facture(client, statut)
47
56
  return new(etape: :facture_introuvable, statut_flux: statut) if facture.nil?
@@ -106,7 +115,8 @@ module Cpro
106
115
 
107
116
  # Nomme la situation en une valeur sur laquelle brancher, l'étape du suivi et l'état de la
108
117
  # facture étant aplatis ensemble. `:deposee` dit que la plateforme a pris la facture sans rien
109
- # signaler — le dépôt a réussi, l'acheminement n'a pas encore eu lieu.
118
+ # signaler. D'après la spécification, elle l'émet alors vers la plateforme du destinataire,
119
+ # mais aucun statut obligatoire ne le confirme : l'acheminement n'est ni prouvé ni exclu.
110
120
  #
111
121
  # Ne repose que sur des codes : statut, code motif, étape.
112
122
  #
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.8"
4
+ VERSION = "0.3.0"
5
5
  end
data/lib/cpro.rb CHANGED
@@ -29,6 +29,7 @@ require_relative "cpro/entities/statut_flux"
29
29
  require_relative "cpro/cdv/statut"
30
30
  require_relative "cpro/cdv/message"
31
31
  require_relative "cpro/facturx"
32
+ require_relative "cpro/controle_adressage"
32
33
  require_relative "cpro/archive"
33
34
  require_relative "cpro/enveloppe"
34
35
  require_relative "cpro/resources/base"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cpro-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.8
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hôtentic
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-09-25 00:00:00.000000000 Z
11
+ date: 2026-10-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -63,6 +63,7 @@ files:
63
63
  - lib/cpro/client.rb
64
64
  - lib/cpro/configuration.rb
65
65
  - lib/cpro/connection.rb
66
+ - lib/cpro/controle_adressage.rb
66
67
  - lib/cpro/entities/base.rb
67
68
  - lib/cpro/entities/changement_statut.rb
68
69
  - lib/cpro/entities/code_routage.rb