cpro-client 0.2.7 → 0.2.8

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: 56e12349c80b29ebc7190354cefea789a6a9114d42260bc40e2851529255a043
4
- data.tar.gz: 35ec2d79aace35593b9493dd6e07b3ede842726ba9b2cc9b63491ca269844272
3
+ metadata.gz: 629795027a48606736ffd0273cd3bf2b1e457237508f428978a4bdbce3245f56
4
+ data.tar.gz: 5a3a218220129860479c710a68aaf4eb612ab74f2f6c8a812a7aa65e2f90f7a0
5
5
  SHA512:
6
- metadata.gz: e3372fac6b533cb8b24b08dbae14c2dce8f495a93d4b53161d553b874ab208bee3099f53efa0af7159e4690cd5aea8414c9da2fef33a38c8cd875796aff99d60
7
- data.tar.gz: fae64931447cd4708e173bca2790890bad33ed65bbc370ad49dc40e60a1f12185af579d947eec2a766fbb79f1dd5da07f77e1c99d18a54678532866b664ae206
6
+ metadata.gz: 23cc35215eb0530f66b71677d35948c1da2896c626a400018901de87c1e2ed8d517ffcb3eefe5f02d5978a7e1f966c513fd8cb1c114aa3950294fb0805915f21
7
+ data.tar.gz: 89a63f46184b02fb02ce615a964148f3ee3997899855312bacd77f3c41d5a7da6cd1b0ee255040fcf2304d3fa05541b13fbb57915ebca9b0c5187b7c07a20b80
data/CHANGELOG.md CHANGED
@@ -1,6 +1,28 @@
1
1
  ## [Unreleased]
2
2
 
3
3
 
4
+ ## [0.2.8] - 2026-09-25
5
+
6
+ ### Ajouté
7
+ - `Cpro::Resources::Factures#search` refuse une pagination hors bornes avant tout appel réseau :
8
+ `limite` au-delà de 50 — le maximum déclaré par le swagger — ou `ignorer` négatif lèvent une
9
+ `ArgumentError`. Relevé en production : au-delà de la borne, la plateforme répond `500 Erreur
10
+ interne` et non un 400, ce qui laisse croire à une panne alors que la requête est en cause.
11
+
12
+ - `Cpro::Suivi#orientation` cesse de traiter `:en_cours` comme un fourre-tout, qui absorbait neuf
13
+ statuts sur vingt-deux — dont `ENCAISSEE`, une facture payée ressortant « en cours ». Deux
14
+ 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.
20
+
21
+ `:en_cours` ne garde que `DEMANDE_DE_PAIEMENT_DIRECT` et les trois statuts d'affacturage, sur
22
+ lesquels nous n'avons ni mesure ni lecture. Un statut posé par le vendeur l'emporte désormais sur
23
+ un `NON_TRANSMISE` devenu caduc, dont l'appelant a déjà fait ce qu'il fallait.
24
+
25
+
4
26
  ## [0.2.7] - 2026-09-07
5
27
 
6
28
  ### Ajouté
data/CHORUS_PRO.md ADDED
@@ -0,0 +1,302 @@
1
+ # Comportements observés de Chorus Pro G2B
2
+
3
+ Ce que la plateforme répond vraiment, scénario par scénario. Les swaggers décrivent les champs, pas
4
+ les enchaînements ; ce fichier comble l'écart, et sert à reconnaître une situation sans avoir à la
5
+ reproduire.
6
+
7
+ Chaque ligne porte sa provenance :
8
+
9
+ - **mesuré** — relevé sur la sandbox PISTE, avec sa date. C'est la source la plus fiable ;
10
+ - **documenté** — tiré du *Dossier de spécifications externes FE — Chorus Pro v1.1* ou de l'annexe
11
+ *Correspondance codes interfaces / CDV / statuts v1.2*, sans avoir été observé.
12
+
13
+ Un comportement documenté mais non mesuré n'est pas une certitude : le swagger de l'Annuaire
14
+ déclarait `contient` là où le serveur exige `strict`, et la consultation d'un code routage ne sert
15
+ pas les champs que sa recherche renvoie.
16
+
17
+ ## Deux niveaux qu'il ne faut pas confondre
18
+
19
+ Un dépôt est jugé **deux fois**, et les deux verdicts vivent dans des API différentes.
20
+
21
+ | | Le flux | La facture |
22
+ | --- | --- | --- |
23
+ | Ce qui est jugé | l'enveloppe : archive, checksum, syntaxe des fichiers | le contenu métier : adressage, unicité, conformité |
24
+ | Où le lire | `GET /flux/{uid}` → `Cpro::Entities::StatutFlux` | Recherche Factures → `Cpro::Entities::Facture` |
25
+ | Statuts | `RECEVABLE` (500), `IRRECEVABLE` (501) | les 22 statuts du cycle de vie |
26
+ | Motifs | `StatutFlux#motifs_rejet` | `Facture#historique` → `ChangementStatut#motifs_rejet` |
27
+
28
+ **`RECEVABLE` ne dit rien du sort de la facture.** Un flux dont la facture référence un vendeur
29
+ inconnu ressort quand même recevable *(mesuré le 2026-08-14)*. `Cpro::Client#suivi` enchaîne les
30
+ deux niveaux et rassemble leurs motifs.
31
+
32
+ ## Tout verdict est asynchrone
33
+
34
+ `POST /flux` ne valide que la requête : un 400 ou un 403 lève immédiatement, un 200 rend un
35
+ `uidFlux` et ne dit rien d'autre. **Aucun statut n'accompagne le dépôt**, pas même l'irrecevabilité —
36
+ contrôler la syntaxe des fichiers suppose d'ouvrir l'archive, ce que le PPF ne fait pas dans le
37
+ temps de la requête.
38
+
39
+ Le verdict se lit ensuite par `GET /flux/{uid}`, documenté `404 Flux non trouvé **ou non traité**` :
40
+ la même réponse pour un flux inexistant et pour un flux en attente de jugement.
41
+
42
+ Écarts relevés entre le moment où la plateforme pose le statut (`dateMajStatut`) et celui où il est
43
+ lisible, d'après les cassettes sandbox :
44
+
45
+ | Cassette | Statut posé | Lu à | Écart |
46
+ | --- | --- | --- | --- |
47
+ | `flux_statut` | 14/08 20:26:02 | 14/08 20:36:58 | ~11 min |
48
+ | `flux_statut_cdv` | 15/08 15:40:11 | 15/08 15:43:31 | ~3 min |
49
+
50
+ Le test sandbox `test_a_processed_flux_is_recevable` interroge d'ailleurs un flux déposé lors d'une
51
+ session antérieure, et non celui que `test_depositing_a_facturx_invoice_returns_a_flux_uid` vient de
52
+ déposer : un flux tout juste déposé n'est pas consultable.
53
+
54
+ Compter en minutes pour le verdict d'enveloppe, davantage pour que la facture apparaisse dans
55
+ Recherche Factures. `Cpro::Client#suivi` rend `:flux_en_attente` pendant toute cette période, au
56
+ coût d'un seul appel réseau.
57
+
58
+ ## Les scénarios
59
+
60
+ Le `→` sépare les étapes successives. Un motif entre crochets est porté par le statut qui le
61
+ précède.
62
+
63
+ | Scénario | Flux | Facture | Issue |
64
+ | --- | --- | --- | --- |
65
+ | **Enveloppe invalide** *(mesuré 15/08)* | `IRRECEVABLE` (501) [`IRR_SYNTAX`] | jamais créée, Recherche Factures répond 204 | terminal — corriger le fichier et redéposer |
66
+ | **Adressage introuvable** *(mesuré 30/08)* | `RECEVABLE` (500) | `REJETEE` [`REJ_ADR`] — seule entrée de l'historique | terminal — la facture existe, son numéro est consommé |
67
+ | **Destinataire sans plateforme** *(mesuré 04/09)* | `RECEVABLE` (500) | `DEPOSEE` [`NON_TRANSMISE`] | **conforme** — jamais transmise, duplicata à la charge de l'émetteur |
68
+ | **Plateforme non raccordée** *(mesuré 04/09)* | `RECEVABLE` (500) | `DEPOSEE` [`NON_TRANSMISE`] | **conforme** — sera transmise au raccordement de la plateforme |
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é |
71
+ | **Non-conformité** *(documenté)* | `RECEVABLE` (500) | `REJETEE` | la facture n'est pas intégrée ; retransmission après correction possible |
72
+ | **Routage impossible** *(documenté)* | `RECEVABLE` (500) | `ERREUR_ROUTAGE` (221) | terminal |
73
+
74
+ Les deux cas `NON_TRANSMISE` portent le **même statut et le même motif** : seule la note les
75
+ distingue, et seul l'annuaire permet de les anticiper.
76
+
77
+ | `statutPlateforme` de la ligne d'annuaire | Note du motif | Ce que fait Chorus Pro |
78
+ | --- | --- | --- |
79
+ | `Plateforme active` | — | transmet |
80
+ | `Plateforme en attente d'activation` | « La plateforme agréée de votre client n'est pas raccordée à CHORUS PRO » | transmettra au raccordement |
81
+ | `Pas de plateforme` | « Le destinataire n'a pas choisi de plateforme agréée » | ne transmettra jamais |
82
+
83
+ C'est pourquoi `Annuaire#find_identifiant_adressage` rend l'adresse **même sans plateforme active**,
84
+ en signalant le cas par `plateforme_active?` : la facture reste déposable, et l'obligation
85
+ réglementaire remplie.
86
+
87
+ ## Le piège du statut porteur
88
+
89
+ Les motifs sont accrochés au statut sous lequel l'incident survient, qui n'a pas à être alarmant.
90
+ Une facture non transmise est `DEPOSEE` :
91
+
92
+ ```ruby
93
+ facture.statut # => "DEPOSEE"
94
+ facture.rejetee? # => false ← et pourtant elle n'arrivera jamais
95
+ facture.en_echec? # => true
96
+ ```
97
+
98
+ Se fier au statut seul fait manquer le cas. `Facture#en_echec?` couvre les statuts d'échec **et**
99
+ tout motif présent dans l'historique ; `#dernier_incident` isole le plus récent, avec ses motifs.
100
+
101
+ Un même changement de statut porte plusieurs motifs — `NON_TRANSMISE` et `REJ_UNI` ont été relevés
102
+ ensemble le 04/09.
103
+
104
+ ## Orienter une facture vers son traitement
105
+
106
+ Le statut seul ne suffit pas : deux couples de cas partagent le même. L'orientation se fait sur le
107
+ couple **statut × motif**.
108
+
109
+ | Cas | Statut | Motif | Comment le distinguer |
110
+ | --- | --- | --- | --- |
111
+ | **En cours** | `DEPOSEE` | aucun | rien à faire, réinterroger |
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
+ | **Rejet définitif** | `REJETEE` | `REJ_ADR` *(mesuré)* | la facture est intégrée, son numéro est consommé : avoir + nouvelle facture |
114
+ | | `ERREUR_ROUTAGE` | — | acheminée mais non routable chez le destinataire |
115
+ | **Transmission différée** | `DEPOSEE` | `NON_TRANSMISE` | note « La plateforme agréée de votre client n'est pas raccordée » |
116
+ | **Jamais transmise** | `DEPOSEE` | `NON_TRANSMISE` | note « Le destinataire n'a pas choisi de plateforme agréée » |
117
+ | **Transmise** | voir liste ci-dessous | — | un statut que seul l'aval peut poser |
118
+
119
+ **La note distingue les deux `NON_TRANSMISE`**, faute de mieux. Plus sûr : interroger l'annuaire
120
+ *avant* le dépôt — `find_identifiant_adressage(siret).statut_plateforme` vaut
121
+ `Plateforme en attente d'activation` dans le premier cas, `Pas de plateforme` dans le second.
122
+
123
+ ### Statuts qui prouvent l'acheminement
124
+
125
+ D'après la colonne « Sens flux » de l'annexe, ces statuts proviennent de la plateforme du
126
+ destinataire ou constatent l'émission par Chorus Pro. Les recevoir prouve que la facture est partie.
127
+
128
+ ```ruby
129
+ TRANSMISE = %w[
130
+ EMISE_PAR_LA_PLATEFORME RECUE_DE_LA_PLATEFORME MISE_A_DISPOSITION PRISE_EN_CHARGE
131
+ APPROUVEE APPROUVEE_PARTIELLEMENT EN_LITIGE SUSPENDUE PAIEMENT_TRANSMIS REFUSEE VISEE
132
+ ].freeze
133
+ ```
134
+
135
+ `APPROUVEE`, `REFUSEE` et `VISEE` circulent **dans les deux sens** : reçus, ils prouvent
136
+ l'acheminement ; émis par vous via `Flux#emettre_statut`, ils ne prouvent rien. Ils figurent quand
137
+ même dans la liste, parce que les recevoir est le cas courant et que les omettre ferait retomber
138
+ une facture refusée par l'acheteur sur « en cours » — ce qui serait faux. Si votre application émet
139
+ l'un d'eux, elle le sait et peut l'écarter.
140
+
141
+ `COMPLETEE`, `ENCAISSEE`, `ANNULEE` et `CHANGEMENT_DE_COMPTE_A_PAYER`, que seul le vendeur pose, ne
142
+ sont en revanche jamais des preuves d'acheminement.
143
+
144
+ ### Mise en œuvre
145
+
146
+ `Cpro::Suivi#orientation` aplatit l'étape du suivi et l'état de la facture en une valeur :
147
+
148
+ | Orientation | Origine | Traitement |
149
+ | --- | --- | --- |
150
+ | `:en_attente` | flux non encore jugé | réinterroger |
151
+ | `:enveloppe_irrecevable` | flux `IRRECEVABLE` | corriger le fichier, redéposer |
152
+ | `: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 |
154
+ | `:cycle_de_vie` | statut posé par le vendeur | la facture vit sa vie commerciale |
155
+ | `:en_cours` | statut non classé | ne rien en déduire |
156
+ | `:rejetee` | statut `REJETEE` | lire les motifs |
157
+ | `:non_transmise` | motif `NON_TRANSMISE` sur l'incident courant | préparer un duplicata |
158
+ | `:erreur_routage` | statut `ERREUR_ROUTAGE` | terminal |
159
+ | `:transmise` | statut de `Suivi::STATUTS_TRANSMISE` | rien |
160
+
161
+ ```ruby
162
+ 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
+ end
169
+ ```
170
+
171
+ L'ordre d'évaluation compte : les statuts passent avant les motifs, si bien qu'un acheminement
172
+ avéré ou un encaissement l'emportent sur un `NON_TRANSMISE` devenu caduc, dont l'appelant a déjà
173
+ fait ce qu'il fallait. Et c'est l'**incident courant** qui décide, non le cumul de l'historique :
174
+ une facture déposée sans être transmise, puis mise à disposition, puis refusée, ressort
175
+ `:transmise`.
176
+
177
+ `:cycle_de_vie` couvre `ENCAISSEE`, `COMPLETEE`, `ANNULEE` et `CHANGEMENT_DE_COMPTE_A_PAYER` — les
178
+ statuts que seul le vendeur pose. Ils ne prouvent pas l'acheminement : on encaisse très bien une
179
+ facture jamais transmise, après remise d'un duplicata.
180
+
181
+ `:en_cours` ne garde que les statuts sur lesquels nous n'avons ni mesure ni lecture —
182
+ `DEMANDE_DE_PAIEMENT_DIRECT` et les trois d'affacturage. Ce n'est pas un fourre-tout : tout autre
183
+ statut a sa place ailleurs.
184
+
185
+ ### Ce que l'orientation ne tranche pas
186
+
187
+ Deux distinctions restent à l'appelant, parce que les porter dans la gem serait fragile.
188
+
189
+ **Les deux `NON_TRANSMISE`.** Seul le texte de la note les sépare, et il est rédigé par l'AIFE :
190
+ une reformulation casserait la classification sans que rien ne le signale. Le moyen sûr est en
191
+ amont — `Annuaire#find_identifiant_adressage(siret).statut_plateforme` vaut
192
+ `Plateforme en attente d'activation` pour le différé, `Pas de plateforme` pour le définitif.
193
+
194
+ **Rejet corrigeable ou définitif.** La distinction repose sur l'idée qu'un rejet de conformité
195
+ n'intègre pas la facture et ne consomme donc pas son numéro. La spécification le laisse entendre,
196
+ nous ne l'avons pas mesuré. `suivi.motifs` permet de trancher chez soi :
197
+
198
+ ```ruby
199
+ definitif = suivi.motifs.any? { |motif| motif.code == "REJ_ADR" }
200
+ ```
201
+
202
+ `REJ_UNI` se superpose aux autres motifs sans les remplacer : il signale que le triptyque était déjà
203
+ pris, ce qui se traite indépendamment de l'orientation.
204
+
205
+ **Ce qui est mesuré et ce qui ne l'est pas.** Seuls `REJETEE`/`REJ_ADR`, `DEPOSEE`/`NON_TRANSMISE`
206
+ dans ses deux variantes et `DEPOSEE`/`REJ_UNI` ont été observés. Le rejet de conformité,
207
+ `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.
209
+
210
+ ## Motifs rencontrés
211
+
212
+ | Code | Libellé | Où | Signification |
213
+ | --- | --- | --- | --- |
214
+ | `IRR_SYNTAX` | Contrôle syntaxique des fichiers du flux | flux | un fichier de l'archive est mal formé ; la note nomme le fichier fautif |
215
+ | `REJ_ADR` | Rejet sur Contrôle d'adressage | facture | BT-49 ne correspond à aucune adresse de l'annuaire |
216
+ | `NON_TRANSMISE` | Destinataire non connecté | facture | adresse valide, destinataire non raccordé |
217
+ | `REJ_UNI` | Contrôle de l'unicité du fichier de facture | facture | le triptyque est déjà utilisé |
218
+ | `ROUTAGE_ERR` | Erreur de routage | facture | destinataire **public** adressé par le code interface G2B |
219
+ | `ADR_ERR` | *(documenté)* | facture | l'adresse électronique est absente de la facture |
220
+
221
+ ## L'unicité, et ce qu'elle implique
222
+
223
+ > le triptyque permettant d'assurer l'unicité de la facture est le suivant : **année d'émission
224
+ > (BT-2)**, **SIREN de l'émetteur (BT-30)**, **numéro de facture (BT-1)**.
225
+
226
+ Le libellé du motif parle du « fichier », mais le contrôle porte sur ces trois valeurs : deux dépôts
227
+ dont seuls la date d'émission et BT-49 différaient ont bien été jugés doublons *(mesuré 04/09)*.
228
+
229
+ **Un numéro est consommé au premier dépôt, quel que soit le sort de celui-ci.** Une facture rejetée
230
+ sur l'adressage occupe son numéro : la corriger et la redéposer sous le même numéro échoue.
231
+
232
+ La spécification prévoit pourtant qu'une facture rejetée pour **non-conformité** peut être
233
+ retransmise après correction — elle précise que dans ce cas « la facture n'est pas intégrée dans la
234
+ solution Chorus Pro ». La lecture cohérente avec nos mesures est donc qu'un rejet de conformité
235
+ n'intègre pas la facture et ne consomme pas son numéro, alors qu'un rejet d'adressage l'intègre et
236
+ le consomme. Nous n'avons mesuré que le second cas.
237
+
238
+ ## Qui émet quoi
239
+
240
+ D'après l'annexe *Correspondance codes interfaces / CDV / statuts*, feuillet G2B. En mode API il n'y
241
+ a **aucune notification** : tout ce qui vient de la plateforme se lit par interrogation.
242
+
243
+ **Émis par nous** (interface `FSO3300A`, via `Flux#emettre_statut`) — les sept statuts du catalogue
244
+ `Cpro::Cdv::Statut`, dont deux obligatoires : `Refusée` (210) et `Encaissée` (212).
245
+
246
+ **Reçus** (interface `FEN4000A` ou `CSO311xA`) — `Déposée` (200), `Émise par la plateforme` (201),
247
+ `Reçue de la plateforme` (202), `Mise à disposition` (203), `Prise en charge` (204), `Approuvée
248
+ partiellement` (206), `En litige` (207), `Suspendue` (208), `Paiement transmis` (211), `Rejetée`
249
+ (213), `Erreur routage` (221). `Approuvée` (205), `Refusée` (210) et `Visée` (214) circulent dans
250
+ les deux sens.
251
+
252
+ Un CDV déposé peut lui-même être rejeté — statut `601`, interface `FEN4002A`. Par quel appel API ce
253
+ rejet se lit reste à déterminer.
254
+
255
+ ## Points pratiques
256
+
257
+ **Lier la facture à ses statuts.** BT-18 (identifiant d'objet facturé) qualifié `AJW` est repris par
258
+ Chorus Pro dans chaque cycle de vie en MDT-92, avec le même qualifiant. C'est un lien plus stable
259
+ que le `nomFlux`, dont le format doit changer.
260
+
261
+ **Allotir.** La spécification recommande un maximum de 500 factures par flux.
262
+
263
+ **Un destinataire public ne se facture pas en G2B.** Relevé en production le 25/09 :
264
+
265
+ > `ROUTAGE_ERR` — « Il n'est pas possible de transmettre des factures intra sphère publique par ce
266
+ > code interface »
267
+
268
+ Le G2B (`FSO3117A`) va du public vers le privé. Une facture destinée à une autre entité publique
269
+ relève du circuit **G2G**, que cette gem ne couvre pas. L'annuaire ne le signale pas à l'adressage :
270
+ `find_identifiant_adressage` rend l'adresse d'une structure publique comme de n'importe quelle
271
+ autre. La nature du destinataire se lit par `search_siren(filtres: { siren: … }).first.type_entite`,
272
+ qui vaut `Publique` ou `Privée assujettie` — et seule la **recherche** la sert, la consultation
273
+ laissant le champ nul.
274
+
275
+ **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.
279
+
280
+ **Une facture peut rester `DEPOSEE` indéfiniment, sans aucun motif.** Relevé en production : une
281
+ 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.
285
+
286
+ **Une requête hors bornes donne un 500, pas un 400.** `limite` est plafonné à 50 sur
287
+ `POST /facture/recherche` ; au-delà, la plateforme répond `500 Erreur interne` sans rien expliquer.
288
+ Le même comportement a été relevé sur l'Annuaire, où un filtre `siret` privé de son opérateur
289
+ donne également un 500. **Ne lisez pas un 500 comme une panne avant d'avoir vérifié votre requête
290
+ contre le swagger.** `Factures#search` refuse désormais localement une pagination hors bornes.
291
+
292
+ **Le 204 de Recherche Factures est ambigu** : aucune facture correspondante, ou compte technique
293
+ rattaché ni à la structure émettrice ni à la destinataire. Rien ne permet de les distinguer.
294
+
295
+ **Les dates.** L'API Flux date en UTC **sans marqueur de fuseau** (`2026-09-04T16:38:00.731094`),
296
+ Recherche Factures avec un `Z` depuis fin août. `Entities::Base#parse_time` absorbe les deux formes.
297
+
298
+ ## Sources
299
+
300
+ - *Dossier de spécifications externes FE — Chorus Pro v1.1*
301
+ - *Annexe Chorus Pro — Correspondance codes interfaces / CDV / statuts v1.2*, feuillet G2B
302
+ - Relevés sandbox PISTE, du 2026-08-14 au 2026-09-04
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- cpro-client (0.2.7)
4
+ cpro-client (0.2.9)
5
5
  faraday (>= 0.9, < 3.0)
6
6
 
7
7
  GEM
@@ -14,6 +14,10 @@ module Cpro
14
14
 
15
15
  TOTAL_KEY = "nombreTotalResultats"
16
16
 
17
+ # Le swagger plafonne `limite` à 50. Au-delà, la plateforme répond 500 et non 400 : mieux
18
+ # vaut refuser localement que d'aller chercher une erreur serveur qui n'explique rien.
19
+ LIMITE_MAX = 50
20
+
17
21
  def search(filtres:, **options)
18
22
  payload = post("facture/recherche", search_body(filtres, options))
19
23
  Entities::ResultatRecherche.new(payload, item_class: Entities::Facture, total_key: TOTAL_KEY)
@@ -39,6 +43,7 @@ module Cpro
39
43
  private
40
44
 
41
45
  def search_body(filtres, options)
46
+ verifier_pagination!(options)
42
47
  compact_body(
43
48
  "filtres" => build_filtres(filtres),
44
49
  "champs" => Array(options[:champs]).map(&:to_s),
@@ -48,6 +53,18 @@ module Cpro
48
53
  )
49
54
  end
50
55
 
56
+ def verifier_pagination!(options)
57
+ limite = options[:limite]
58
+ if !limite.nil? && (limite.to_i.negative? || limite.to_i > LIMITE_MAX)
59
+ raise ArgumentError, "limite hors bornes : #{limite.inspect} (0 à #{LIMITE_MAX})"
60
+ end
61
+
62
+ ignorer = options[:ignorer]
63
+ return if ignorer.nil? || !ignorer.to_i.negative?
64
+
65
+ raise ArgumentError, "ignorer ne peut pas être négatif : #{ignorer.inspect}"
66
+ end
67
+
51
68
  def build_filtres(filtres)
52
69
  raise ArgumentError, "au moins un filtre est requis" if filtres.nil? || filtres.empty?
53
70
 
data/lib/cpro/suivi.rb CHANGED
@@ -10,8 +10,8 @@ module Cpro
10
10
  class Suivi
11
11
  ETAPES = %i[flux_en_attente flux_irrecevable facture_introuvable facture_connue].freeze
12
12
 
13
- ORIENTATIONS = %i[en_attente enveloppe_irrecevable facture_introuvable en_cours
14
- rejetee non_transmise erreur_routage transmise].freeze
13
+ ORIENTATIONS = %i[en_attente enveloppe_irrecevable facture_introuvable deposee en_cours
14
+ rejetee non_transmise erreur_routage cycle_de_vie transmise].freeze
15
15
 
16
16
  # Statuts qui prouvent que la facture a quitté Chorus Pro : d'après la colonne « Sens flux »
17
17
  # de l'annexe des statuts, ils proviennent de la plateforme du destinataire ou constatent
@@ -25,8 +25,14 @@ module Cpro
25
25
  APPROUVEE APPROUVEE_PARTIELLEMENT EN_LITIGE SUSPENDUE PAIEMENT_TRANSMIS REFUSEE VISEE
26
26
  ].freeze
27
27
 
28
+ # Statuts que seul le vendeur pose, via `Flux#emettre_statut`. La facture vit sa vie
29
+ # commerciale — mais ils ne prouvent pas l'acheminement : on peut encaisser une facture jamais
30
+ # transmise, après remise d'un duplicata. C'est même le parcours que décrit la spécification.
31
+ STATUTS_CYCLE_DE_VIE = %w[ENCAISSEE COMPLETEE ANNULEE CHANGEMENT_DE_COMPTE_A_PAYER].freeze
32
+
28
33
  MOTIF_NON_TRANSMISE = "NON_TRANSMISE"
29
34
  STATUT_ERREUR_ROUTAGE = "ERREUR_ROUTAGE"
35
+ STATUT_DEPOSEE = "DEPOSEE"
30
36
 
31
37
  attr_reader :etape, :statut_flux, :facture
32
38
 
@@ -99,7 +105,10 @@ module Cpro
99
105
  end
100
106
 
101
107
  # Nomme la situation en une valeur sur laquelle brancher, l'étape du suivi et l'état de la
102
- # facture étant aplatis ensemble. Ne repose que sur des codes — statut, code motif, étape.
108
+ # 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.
110
+ #
111
+ # Ne repose que sur des codes : statut, code motif, étape.
103
112
  #
104
113
  # Deux distinctions sont délibérément laissées à l'appelant, parce que les porter ici serait
105
114
  # fragile :
@@ -133,13 +142,19 @@ module Cpro
133
142
 
134
143
  private
135
144
 
136
- # L'ordre compte : un statut d'acheminement l'emporte sur un NON_TRANSMISE devenu caduc, et
145
+ # L'ordre compte. Les statuts passent avant les motifs : un acheminement avéré ou un encaissement
146
+ # l'emportent sur un NON_TRANSMISE devenu caduc, dont l'hôte a déjà fait ce qu'il fallait. Et
137
147
  # c'est l'incident courant — non le cumul de l'historique — qui décrit la situation présente.
148
+ #
149
+ # `:en_cours` ne reste que pour les statuts sur lesquels nous n'avons ni mesure ni lecture :
150
+ # les trois d'affacturage et DEMANDE_DE_PAIEMENT_DIRECT. Ce n'est pas un fourre-tout.
138
151
  def orientation_facture
139
152
  return :erreur_routage if facture.statut == STATUT_ERREUR_ROUTAGE
140
153
  return :rejetee if facture.rejetee?
141
154
  return :transmise if STATUTS_TRANSMISE.include?(facture.statut)
155
+ return :cycle_de_vie if STATUTS_CYCLE_DE_VIE.include?(facture.statut)
142
156
  return :non_transmise if non_transmise?
157
+ return :deposee if facture.statut == STATUT_DEPOSEE
143
158
 
144
159
  :en_cours
145
160
  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.7"
4
+ VERSION = "0.2.8"
5
5
  end
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.7
4
+ version: 0.2.8
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-07 00:00:00.000000000 Z
11
+ date: 2026-09-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -41,6 +41,7 @@ files:
41
41
  - ".gitignore"
42
42
  - ".rubocop.yml"
43
43
  - CHANGELOG.md
44
+ - CHORUS_PRO.md
44
45
  - CODE_OF_CONDUCT.md
45
46
  - Gemfile
46
47
  - Gemfile.lock