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 +4 -4
- data/CHANGELOG.md +65 -5
- data/CHORUS_PRO.md +42 -15
- data/Gemfile.lock +1 -1
- data/README.md +92 -16
- data/lib/cpro/adressage.rb +10 -2
- data/lib/cpro/controle_adressage.rb +117 -0
- data/lib/cpro/entities/ligne_annuaire.rb +10 -0
- data/lib/cpro/errors.rb +16 -0
- data/lib/cpro/facturx.rb +25 -0
- data/lib/cpro/middleware/raise_error.rb +9 -1
- data/lib/cpro/resources/annuaire.rb +13 -4
- data/lib/cpro/resources/flux.rb +8 -2
- data/lib/cpro/suivi.rb +12 -2
- data/lib/cpro/version.rb +1 -1
- data/lib/cpro.rb +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2f8a0732008b70f7a94ea7689d1ecd6675d6a8f6203330ada88e66976baf3d0c
|
|
4
|
+
data.tar.gz: 0fbef1fe26a7ea78cbfe8b5e472dbe5daa6ebb09ce166d00f9133932f7f03c79
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
17
|
-
- **`:cycle_de_vie`** — `ENCAISSEE`, `COMPLETEE`, `ANNULEE`, `CHANGEMENT_DE_COMPTE_A_PAYER`, les
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
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
|
-
| **
|
|
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
|
|
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, :
|
|
164
|
-
when :enveloppe_irrecevable, :rejetee
|
|
165
|
-
when :non_transmise
|
|
166
|
-
when :erreur_routage
|
|
167
|
-
when :transmise
|
|
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.
|
|
277
|
-
|
|
278
|
-
|
|
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
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
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
|
|
147
|
-
|
|
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
|
-
|
|
159
|
-
|
|
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, :
|
|
292
|
-
when :enveloppe_irrecevable, :rejetee
|
|
293
|
-
when :non_transmise
|
|
294
|
-
when :erreur_routage
|
|
295
|
-
when :transmise
|
|
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` |
|
|
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
|
|
data/lib/cpro/adressage.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
#
|
|
116
|
-
#
|
|
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
|
-
|
|
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)
|
data/lib/cpro/resources/flux.rb
CHANGED
|
@@ -3,8 +3,14 @@
|
|
|
3
3
|
module Cpro
|
|
4
4
|
module Resources
|
|
5
5
|
class Flux < Base
|
|
6
|
-
|
|
7
|
-
|
|
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)
|
|
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
|
|
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
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.
|
|
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-
|
|
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
|