cpro-client 0.1.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.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +10 -0
  3. data/.rubocop.yml +42 -0
  4. data/CHANGELOG.md +29 -0
  5. data/CODE_OF_CONDUCT.md +84 -0
  6. data/Gemfile +13 -0
  7. data/Gemfile.lock +79 -0
  8. data/LICENSE.txt +21 -0
  9. data/README.md +310 -0
  10. data/Rakefile +25 -0
  11. data/bin/console +15 -0
  12. data/bin/setup +8 -0
  13. data/cpro-client.gemspec +32 -0
  14. data/lib/cpro/account.rb +32 -0
  15. data/lib/cpro/archive.rb +44 -0
  16. data/lib/cpro/auth/token.rb +36 -0
  17. data/lib/cpro/auth/token_provider.rb +59 -0
  18. data/lib/cpro/cdv/message.rb +230 -0
  19. data/lib/cpro/cdv/statut.rb +80 -0
  20. data/lib/cpro/client.rb +44 -0
  21. data/lib/cpro/configuration.rb +89 -0
  22. data/lib/cpro/connection.rb +19 -0
  23. data/lib/cpro/entities/base.rb +71 -0
  24. data/lib/cpro/entities/changement_statut.rb +27 -0
  25. data/lib/cpro/entities/depot_flux.rb +16 -0
  26. data/lib/cpro/entities/etablissement.rb +96 -0
  27. data/lib/cpro/entities/facture.rb +85 -0
  28. data/lib/cpro/entities/ligne_annuaire.rb +42 -0
  29. data/lib/cpro/entities/lignes_annuaire.rb +45 -0
  30. data/lib/cpro/entities/motif_rejet.rb +24 -0
  31. data/lib/cpro/entities/note_statut.rb +24 -0
  32. data/lib/cpro/entities/resultat_recherche.rb +55 -0
  33. data/lib/cpro/entities/statut_flux.rb +54 -0
  34. data/lib/cpro/entities/unite_legale.rb +47 -0
  35. data/lib/cpro/enveloppe.rb +73 -0
  36. data/lib/cpro/errors.rb +25 -0
  37. data/lib/cpro/facturx.rb +53 -0
  38. data/lib/cpro/middleware/authentication.rb +51 -0
  39. data/lib/cpro/middleware/raise_error.rb +50 -0
  40. data/lib/cpro/resources/annuaire.rb +110 -0
  41. data/lib/cpro/resources/base.rb +61 -0
  42. data/lib/cpro/resources/factures.rb +65 -0
  43. data/lib/cpro/resources/flux.rb +60 -0
  44. data/lib/cpro/version.rb +5 -0
  45. data/lib/cpro-client.rb +3 -0
  46. data/lib/cpro.rb +56 -0
  47. metadata +106 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a797bddcc8e42f168e7c6ac7e18f6a02de3f45b6dbe6bc03f4eb7f11a3854800
4
+ data.tar.gz: 25effc0a7fd95d27528d48c20d1289e236717ad7efbe3318d9a3284033ecf720
5
+ SHA512:
6
+ metadata.gz: '095e98ac8c3752f4a117aab59f74bb77a15cd506bd5621637185e857261c8471d53850cbf5d42e2fcf72f054435ced5d060a26c659942224d80d3561813ea189'
7
+ data.tar.gz: e74b20cba8005d0d7b14b178d8e8e80e48b8f96a84f6c5cd859f7465abdaa1d1ed58d787c659a378b0896a592de9a61ee645266128d1241922a3665211992df2
data/.gitignore ADDED
@@ -0,0 +1,10 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ /.idea/
10
+ /local/
data/.rubocop.yml ADDED
@@ -0,0 +1,42 @@
1
+ AllCops:
2
+ TargetRubyVersion: 2.7
3
+ NewCops: enable
4
+ SuggestExtensions: false
5
+
6
+ Style/StringLiterals:
7
+ Enabled: true
8
+ EnforcedStyle: double_quotes
9
+
10
+ Style/StringLiteralsInInterpolation:
11
+ Enabled: true
12
+ EnforcedStyle: double_quotes
13
+
14
+ Style/Documentation:
15
+ Enabled: false
16
+
17
+ Layout/LineLength:
18
+ Max: 120
19
+
20
+ # Le fichier doit porter le nom de la gem pour que `require "cpro-client"` fonctionne.
21
+ Naming/FileName:
22
+ Exclude:
23
+ - "lib/cpro-client.rb"
24
+
25
+ # Un test lisible enchaîne les assertions : ces métriques n'y ont pas de sens.
26
+ Metrics:
27
+ Exclude:
28
+ - "test/**/*"
29
+
30
+ Metrics/AbcSize:
31
+ Max: 20
32
+
33
+ # Les mots-clés sont auto-documentés : les compter revient à pénaliser une signature claire.
34
+ Metrics/ParameterLists:
35
+ CountKeywordArgs: false
36
+
37
+ # Une classe qui transpose une spécification champ à champ dépasse légitimement 100 lignes.
38
+ Metrics/ClassLength:
39
+ Max: 180
40
+
41
+ Metrics/MethodLength:
42
+ Max: 20
data/CHANGELOG.md ADDED
@@ -0,0 +1,29 @@
1
+ ## [Unreleased]
2
+
3
+ - Authentification OAuth2 PISTE (`client_credentials`) avec cache du jeton, renouvellement anticipé
4
+ et rejeu unique d'un appel refusé en 401.
5
+ - Compte technique `cpro-account` scopé par appel via `Cpro::Client#with_account`.
6
+ - API Annuaire G2B : consultation par SIRET et par SIREN, recherche multi-critères, lignes
7
+ d'annuaire paginées.
8
+ - API Dépôt Flux G2B : transmission d'une facture Factur-X (archive tar.gz, base64, checksum
9
+ sha256) et consultation du statut du flux.
10
+ - Émission de statuts de cycle de vie : génération d'un message CDAR D22B conforme à l'Annexe A de
11
+ XP Z12-012, validé contre le XSD et le Schematron BR-FR-CDV, et accepté en sandbox (RECEVABLE).
12
+ Les motifs de rejet et les données conditionnelles sont contrôlés avant tout appel réseau.
13
+ - Motif d'irrecevabilité d'un flux exposé par `Cpro::Entities::StatutFlux#motifs_rejet` et
14
+ `#code_statut`, suivant l'évolution de l'API livrée en Chorus Pro 5.2.2.
15
+ - Contrôle du fichier avant dépôt Factur-X (PDF et pièce jointe présents) et lecture du montant
16
+ TTC déclaré par le XML embarqué, via `Cpro::Facturx`.
17
+ - Dépôt des quatre types de flux acceptés par `POST /flux` : Factur-X, UBL, cycle de vie (CDV) et
18
+ e-reporting. `Cpro::Facturx::Depot` devient `Cpro::Enveloppe`, paramétrée par code interface, et
19
+ `Cpro::Facturx::Archive` devient `Cpro::Archive`.
20
+ - API Recherche Factures G2B : recherche multi-critères, consultation d'une facture avec
21
+ l'historique de ses statuts et les motifs de rejet AFNOR, montants en `BigDecimal`.
22
+ - Tests sandbox rejouables hors ligne via cassettes VCR expurgées (`rake test:sandbox`), et mode
23
+ enregistrement contre la vraie sandbox PISTE (`CPRO_RECORD=1`).
24
+ - Hiérarchie d'erreurs `Cpro::Error` exposant statut HTTP, corps de réponse et identifiant de
25
+ corrélation PISTE.
26
+
27
+ ## [0.1.0] - 2026-08-14
28
+
29
+ - Initial release
@@ -0,0 +1,84 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
6
+
7
+ We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
8
+
9
+ ## Our Standards
10
+
11
+ Examples of behavior that contributes to a positive environment for our community include:
12
+
13
+ * Demonstrating empathy and kindness toward other people
14
+ * Being respectful of differing opinions, viewpoints, and experiences
15
+ * Giving and gracefully accepting constructive feedback
16
+ * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
17
+ * Focusing on what is best not just for us as individuals, but for the overall community
18
+
19
+ Examples of unacceptable behavior include:
20
+
21
+ * The use of sexualized language or imagery, and sexual attention or
22
+ advances of any kind
23
+ * Trolling, insulting or derogatory comments, and personal or political attacks
24
+ * Public or private harassment
25
+ * Publishing others' private information, such as a physical or email
26
+ address, without their explicit permission
27
+ * Other conduct which could reasonably be considered inappropriate in a
28
+ professional setting
29
+
30
+ ## Enforcement Responsibilities
31
+
32
+ Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
33
+
34
+ Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
35
+
36
+ ## Scope
37
+
38
+ This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
39
+
40
+ ## Enforcement
41
+
42
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at jbvilain@gmail.com. All complaints will be reviewed and investigated promptly and fairly.
43
+
44
+ All community leaders are obligated to respect the privacy and security of the reporter of any incident.
45
+
46
+ ## Enforcement Guidelines
47
+
48
+ Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
49
+
50
+ ### 1. Correction
51
+
52
+ **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
53
+
54
+ **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
55
+
56
+ ### 2. Warning
57
+
58
+ **Community Impact**: A violation through a single incident or series of actions.
59
+
60
+ **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
61
+
62
+ ### 3. Temporary Ban
63
+
64
+ **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior.
65
+
66
+ **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
67
+
68
+ ### 4. Permanent Ban
69
+
70
+ **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
71
+
72
+ **Consequence**: A permanent ban from any sort of public interaction within the community.
73
+
74
+ ## Attribution
75
+
76
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0,
77
+ available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
78
+
79
+ Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
80
+
81
+ [homepage]: https://www.contributor-covenant.org
82
+
83
+ For answers to common questions about this code of conduct, see the FAQ at
84
+ https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations.
data/Gemfile ADDED
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in cpro-client.gemspec
6
+ gemspec
7
+
8
+ gem "minitest", "~> 5.0"
9
+ gem "nokogiri", "~> 1.13"
10
+ gem "rake", "~> 13.0"
11
+ gem "rubocop", "~> 1.7"
12
+ gem "vcr", "~> 6.0"
13
+ gem "webmock", "~> 3.0"
data/Gemfile.lock ADDED
@@ -0,0 +1,79 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ cpro-client (0.1.0)
5
+ faraday (~> 2.0)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ addressable (2.9.0)
11
+ public_suffix (>= 2.0.2, < 8.0)
12
+ ast (2.4.3)
13
+ base64 (0.3.0)
14
+ bigdecimal (4.1.2)
15
+ crack (1.0.1)
16
+ bigdecimal
17
+ rexml
18
+ faraday (2.8.1)
19
+ base64
20
+ faraday-net_http (>= 2.0, < 3.1)
21
+ ruby2_keywords (>= 0.0.4)
22
+ faraday-net_http (3.0.2)
23
+ hashdiff (1.2.1)
24
+ json (2.21.2)
25
+ language_server-protocol (3.17.0.6)
26
+ lint_roller (1.1.0)
27
+ minitest (5.26.1)
28
+ nokogiri (1.15.7-x86_64-darwin)
29
+ racc (~> 1.4)
30
+ parallel (1.28.0)
31
+ parser (3.3.12.0)
32
+ ast (~> 2.4.1)
33
+ racc
34
+ prism (1.9.0)
35
+ public_suffix (5.1.1)
36
+ racc (1.8.1)
37
+ rainbow (3.1.1)
38
+ rake (13.4.2)
39
+ regexp_parser (2.12.0)
40
+ rexml (3.4.4)
41
+ rubocop (1.89.0)
42
+ json (~> 2.3)
43
+ language_server-protocol (~> 3.17.0.2)
44
+ lint_roller (~> 1.1.0)
45
+ parallel (>= 1.10)
46
+ parser (>= 3.3.0.2)
47
+ rainbow (>= 2.2.2, < 4.0)
48
+ regexp_parser (>= 2.9.3, < 3.0)
49
+ rubocop-ast (>= 1.49.0, < 2.0)
50
+ ruby-progressbar (~> 1.7)
51
+ unicode-display_width (>= 2.4.0, < 4.0)
52
+ rubocop-ast (1.50.0)
53
+ parser (>= 3.3.7.2)
54
+ prism (~> 1.7)
55
+ ruby-progressbar (1.13.0)
56
+ ruby2_keywords (0.0.5)
57
+ unicode-display_width (3.2.0)
58
+ unicode-emoji (~> 4.1)
59
+ unicode-emoji (4.2.0)
60
+ vcr (6.4.0)
61
+ webmock (3.26.2)
62
+ addressable (>= 2.8.0)
63
+ crack (>= 0.3.2)
64
+ hashdiff (>= 0.4.0, < 2.0.0)
65
+
66
+ PLATFORMS
67
+ x86_64-darwin-21
68
+
69
+ DEPENDENCIES
70
+ cpro-client!
71
+ minitest (~> 5.0)
72
+ nokogiri (~> 1.13)
73
+ rake (~> 13.0)
74
+ rubocop (~> 1.7)
75
+ vcr (~> 6.0)
76
+ webmock (~> 3.0)
77
+
78
+ BUNDLED WITH
79
+ 2.4.5
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Jean-Baptiste Vilain
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,310 @@
1
+ # Cpro::Client
2
+
3
+ Client Ruby des API **G2B** de Chorus Pro (Portail Public de Facturation), exposées via la
4
+ plateforme [PISTE](https://piste.gouv.fr/). La gem couvre le raccordement en mode API ; la
5
+ connexion EDI est hors périmètre.
6
+
7
+ Scénario couvert :
8
+
9
+ 1. authentification OAuth2 auprès de PISTE ;
10
+ 2. recherche d'une structure dans l'annuaire par SIRET ou par SIREN ;
11
+ 3. transmission d'une facture Factur-X et suivi du flux.
12
+
13
+ ## Installation
14
+
15
+ ```ruby
16
+ gem "cpro-client"
17
+ ```
18
+
19
+ ## Configuration
20
+
21
+ Les identifiants PISTE identifient l'application et sont stables :
22
+
23
+ ```ruby
24
+ Cpro.configure do |config|
25
+ config.environment = :sandbox # ou :production
26
+ config.client_id = ENV["PISTE_CLIENT_ID"]
27
+ config.client_secret = ENV["PISTE_CLIENT_SECRET"]
28
+ end
29
+ ```
30
+
31
+ Le jeton OAuth est obtenu à la demande, renouvelé automatiquement, et **mis en cache sur la
32
+ configuration** — pas sur le client. Construire un `Cpro.client` à chaque requête ne redemande donc
33
+ pas de jeton. Un appel refusé en 401 est rejoué une fois avec un jeton neuf.
34
+
35
+ ## Compte technique
36
+
37
+ Toutes les API G2B exigent, en plus du jeton OAuth, un **compte technique** Chorus Pro transmis
38
+ dans l'en-tête `cpro-account`. Le swagger Annuaire ne le déclare pas, mais l'API le rejette en 400
39
+ sans lui — vérifié en sandbox.
40
+
41
+ Comme le compte peut varier d'un appel à l'autre, il se passe par `with_account` plutôt que d'être
42
+ figé à la configuration. Un client sans compte lève une `Cpro::ConfigurationError` avant tout appel
43
+ réseau.
44
+
45
+ ```ruby
46
+ compte = Cpro::Account.new(login: "...", password: "...")
47
+ client = Cpro.client.with_account(compte)
48
+ ```
49
+
50
+ Le compte technique doit être rattaché à une structure publique, faute de quoi l'API répond
51
+ `403 Aucune structure publique ou opérateur public associé au compte technique.`
52
+
53
+ ## Annuaire
54
+
55
+ ```ruby
56
+ etablissement = client.annuaire.find_by_siret("70204275500240")
57
+ etablissement.denomination
58
+ etablissement.publique?
59
+ etablissement.gestion_engagement_juridique? # numéro d'engagement obligatoire ?
60
+ etablissement.lignes_annuaire.map(&:identifiant_routage)
61
+
62
+ unite = client.annuaire.find_by_siren("702042755")
63
+ ```
64
+
65
+ Les lignes d'annuaire sont paginées :
66
+
67
+ ```ruby
68
+ lignes = client.annuaire.find_by_siret(siret, limite: 50, ignorer: 50).lignes_annuaire
69
+ lignes.nombre_total
70
+ lignes.suite? # reste-t-il des lignes au-delà de cette page ?
71
+ ```
72
+
73
+ Recherche multi-critères — l'opérateur de comparaison (`contient` ou `strict`) est déduit du champ :
74
+
75
+ ```ruby
76
+ resultat = client.annuaire.search_siret(
77
+ filtres: { denomination: "MINISTERE", etatAdministratif: "A" },
78
+ tris: { siret: :ascendant },
79
+ limite: 20
80
+ )
81
+
82
+ resultat.nombre_total
83
+ resultat.map(&:siret)
84
+ ```
85
+
86
+ ## Transmission d'une facture Factur-X
87
+
88
+ ```ruby
89
+ depot = client.flux.deposit_facturx("facture.pdf")
90
+ depot.uid # => "b18e3b6c-ccb7-4308-b527-35e5e6ee2145"
91
+ ```
92
+
93
+ `deposit_facturx` accepte un chemin ou un objet IO, empaquette le PDF/A-3 dans l'archive `tar.gz`
94
+ attendue par le PPF, l'encode en base64 et calcule le checksum sha256.
95
+
96
+ La gem ne valide pas le contenu Factur-X : le fichier fourni doit déjà être conforme. Elle vérifie
97
+ seulement, avant l'appel, qu'il s'agit bien d'un PDF et qu'un XML Factur-X y est joint — un
98
+ garde-fou contre le mauvais fichier, dont le rejet ne serait connu que plusieurs minutes plus tard.
99
+
100
+ Le montant TTC déclaré par le XML embarqué est exposé, à confronter à ce que le logiciel hôte
101
+ attend :
102
+
103
+ ```ruby
104
+ Cpro::Facturx.montant_total(File.binread("facture.pdf")) # => BigDecimal("1910.40")
105
+ ```
106
+
107
+ Le même endpoint accepte trois autres types de flux, qui ne diffèrent que par leur code interface :
108
+
109
+ ```ruby
110
+ client.flux.deposit_ubl("facture.xml") # FSO3110A — facture UBL 2.1
111
+ client.flux.deposit_cdv("statut.xml") # FSO3300A — cycle de vie (CDAR D22B)
112
+ client.flux.deposit_ereporting("donnees.xml") # FSO6000A — e-reporting
113
+ ```
114
+
115
+ Émettre un statut de cycle de vie n'est donc pas un appel de mise à jour : c'est un dépôt de flux.
116
+ Le fichier attendu est un CDAR D22B conforme à l'Annexe A de la norme AFNOR XP Z12-012 ; la gem le
117
+ transporte sans le fabriquer.
118
+
119
+ Le PPF répond 404 tant que le flux n'est pas traité. `status` renvoie donc `nil` dans ce cas, ce qui
120
+ rend le polling naturel ; `status!` lève une `Cpro::NotFoundError` si vous préférez l'exception.
121
+
122
+ ```ruby
123
+ statut = client.flux.status(depot)
124
+
125
+ statut&.recevable?
126
+ statut&.nom_flux # renseigné uniquement si le flux est recevable
127
+ statut&.date_maj_statut
128
+ ```
129
+
130
+ Depuis Chorus Pro 5.2.2, un flux irrecevable expose son motif — auparavant le rejet était muet et
131
+ ne pouvait se diagnostiquer que hors ligne :
132
+
133
+ ```ruby
134
+ statut.motifs_rejet.each do |motif|
135
+ motif.code # "IRR_SYNTAX"
136
+ motif.libelle # "Contrôle syntaxique des fichiers du flux"
137
+ motif.notes.map { |note| [note.sujet, note.contenu] }
138
+ end
139
+ ```
140
+
141
+ ## Suivre la facture après le dépôt
142
+
143
+ Le statut du flux ne dit que la recevabilité de l'enveloppe. Le sort de la facture elle-même se lit
144
+ via l'API Recherche Factures, en repartant du `nomFlux` :
145
+
146
+ ```ruby
147
+ resultat = client.factures.search_by_nom_flux(statut) # accepte un StatutFlux ou une chaîne
148
+ facture = client.factures.find(resultat.first)
149
+
150
+ facture.statut # "MISE_A_DISPOSITION"
151
+ facture.encaissee? # un prédicat par statut, sur les 22 de l'énumération
152
+ facture.montant_total # BigDecimal
153
+ facture.historique.map { |changement| [changement.statut, changement.date] }
154
+ facture.motifs_rejet.map(&:to_s) # codes AFNOR et libellés, vides si aucun rejet
155
+ ```
156
+
157
+ Les filtres de recherche sont **plats**, contrairement à ceux de l'Annuaire, et au moins un est
158
+ obligatoire. Un filtre inconnu lève une `ArgumentError` avant tout appel réseau :
159
+
160
+ ```ruby
161
+ client.factures.search(
162
+ filtres: { siretVendeur: "79614076743109", dateEmissionFactureDu: "2026-01-01" },
163
+ tris: { dateEmissionFacture: :descendant },
164
+ limite: 50, ignorer: 0
165
+ )
166
+ ```
167
+
168
+ L'historique n'est renseigné que par `find` ; la recherche ne le retourne pas.
169
+
170
+ Deux points de vigilance. Un **résultat vide est ambigu** : l'API répond `204` aussi bien lorsque
171
+ aucune facture ne correspond que lorsque le compte technique n'est rattaché ni à la structure
172
+ émettrice ni à la destinataire. Et il **n'existe aucun mécanisme de notification** — ni webhook, ni
173
+ callback, et les statuts des factures G2B ne sont pas consultables sur le portail Chorus Pro : le
174
+ suivi passe nécessairement par une interrogation périodique de cette API.
175
+
176
+ ## Servir plusieurs entités
177
+
178
+ L'AIFE distingue trois types de raccordement API. Celui qui s'applique détermine le modèle à
179
+ adopter.
180
+
181
+ **Concentrateur (éditeur clients légers)** — une solution web qui passe les appels pour le compte de
182
+ ses clients. *« Seul le concentrateur a besoin d'une application sur PISTE ainsi que d'un
183
+ raccordement Chorus Pro »*, et chaque entité cliente dispose de son propre compte technique. Une
184
+ seule configuration suffit donc, et `with_account` porte l'entité — il partage le jeton OAuth et les
185
+ connexions HTTP :
186
+
187
+ ```ruby
188
+ client.with_account(compte_mairie).flux.deposit_facturx(facture_a)
189
+ client.with_account(compte_departement).flux.deposit_facturx(facture_b)
190
+ ```
191
+
192
+ **Éditeur clients lourds** — la solution est installée chez chaque client, qui appelle depuis ses
193
+ propres serveurs. Chaque client a alors sa propre application PISTE et son propre raccordement : il
194
+ lui faut sa configuration, donc son client. Conservez-les plutôt que de les reconstruire à chaque
195
+ requête, le cache de jeton vivant sur la configuration.
196
+
197
+ ```ruby
198
+ CLIENTS = entites.to_h do |entite|
199
+ configuration = Cpro::Configuration.new
200
+ configuration.client_id = entite.piste_client_id
201
+ configuration.client_secret = entite.piste_client_secret
202
+ configuration.account = Cpro::Account.new(login: entite.login, password: entite.password)
203
+
204
+ [entite.id, Cpro::Client.new(configuration)]
205
+ end
206
+ ```
207
+
208
+ Dans les deux cas, l'AIFE recommande que **chaque entité cliente crée elle-même son compte
209
+ technique** et en transmette les identifiants, plutôt que l'éditeur ne se rattache aux structures de
210
+ ses clients — pratique explicitement déconseillée pour des raisons de sécurité.
211
+
212
+ ## Erreurs
213
+
214
+ Toutes les erreurs dérivent de `Cpro::Error`.
215
+
216
+ | Exception | Déclenchement |
217
+ | --- | --- |
218
+ | `Cpro::ConfigurationError` | configuration incomplète, compte technique manquant |
219
+ | `Cpro::RequestError` | 400, 406, 415, 422 |
220
+ | `Cpro::AuthenticationError` | 401, échec d'obtention du jeton |
221
+ | `Cpro::AuthorizationError` | 403 — habilitations insuffisantes |
222
+ | `Cpro::NotFoundError` | 404 |
223
+ | `Cpro::RateLimitError` | 429 |
224
+ | `Cpro::ServerError` | 5xx |
225
+
226
+ Les erreurs d'API exposent `#status`, `#body` et `#correlation_id` — ce dernier est l'identifiant
227
+ PISTE à fournir au support en cas d'incident.
228
+
229
+ ## Développement
230
+
231
+ ```bash
232
+ bin/setup
233
+ bundle exec rake # tests unitaires + rubocop, aucun appel réseau
234
+ ```
235
+
236
+ Les tests unitaires sont entièrement stubbés avec WebMock.
237
+
238
+ ### Conformité des messages CDAR
239
+
240
+ Les cycles de vie produits par la gem sont confrontés au schéma de la norme CDAR D22B **et** au
241
+ Schematron des règles françaises BR-FR-CDV. Les ressources sont versionnées dans
242
+ [test/resources/cdar](test/resources/cdar) (Apache 2.0, dépôt FNFE France_RFE), donc ce contrôle
243
+ tourne en CI sans installation ni accès réseau.
244
+
245
+ La validation de schéma passe par Nokogiri. Le Schematron étant en XSLT 2.0, il demande Saxon : à
246
+ défaut, cette moitié du test se saute au lieu d'échouer.
247
+
248
+ ```bash
249
+ brew install saxon # macOS
250
+ apt-get install -y libsaxonhe-java # Debian/Ubuntu
251
+ ```
252
+
253
+ ### Tests sandbox
254
+
255
+ `test/sandbox/` rejoue des échanges réellement enregistrés sur la sandbox PISTE, via des cassettes
256
+ VCR commitées. Hors ligne, sans identifiants, exécutable en CI :
257
+
258
+ ```bash
259
+ bundle exec rake test:sandbox
260
+ ```
261
+
262
+ Toute requête absente des cassettes fait échouer le test plutôt que de partir sur le réseau. Pour
263
+ ré-enregistrer après une évolution de l'API :
264
+
265
+ ```bash
266
+ CPRO_RECORD=1 bundle exec rake test:sandbox
267
+ ```
268
+
269
+ Le mode enregistrement exige `local/credentials.env` et **dépose réellement une facture** dans la
270
+ sandbox. Les cassettes sont expurgées à l'écriture : identifiants PISTE et compte technique
271
+ remplacés par des jetons de substitution, en-têtes `Authorization` et `cpro-account` supprimés,
272
+ `access_token` écrasé. Un `CPRO_CREDENTIALS_FILE` permet de pointer un autre fichier d'identifiants.
273
+
274
+ ## Vérifié en sandbox
275
+
276
+ Chaîne complète validée le 14/08/2026 : dépôt d'un Factur-X accepté (`uidFlux` retourné) puis flux
277
+ `RECEVABLE`.
278
+
279
+ - Le checksum est bien le **sha256 de la chaîne base64**, pas des octets de l'archive — confirmé par
280
+ un flux recevable, le PPF ayant validé l'intégrité.
281
+ - Le nom de l'archive `tar.gz` n'est soumis à aucune règle ; celui du fichier qu'elle contient garde
282
+ son extension d'origine.
283
+ - `cpro-account` est exigé par l'Annuaire aussi, contrairement à son swagger.
284
+ - Chorus Pro 5.2.2 (19/08/2026) ajoute `codeStatut` et `detailStatut` à la consultation de flux, et
285
+ annonce un changement de format du `nomFlux` — n'écrivez donc rien qui présuppose son préfixe.
286
+ - Les API datent en **UTC sans marquer le fuseau** (`2026-08-14T20:26:02.125919`). La gem le
287
+ rattrape : sans cela, la même réponse donnerait un instant différent selon le fuseau du serveur
288
+ hôte.
289
+ - La **recevabilité porte sur l'enveloppe**, pas sur le contenu métier : un flux dont le vendeur est
290
+ inconnu du destinataire ressort quand même `RECEVABLE`. Le sort de la facture elle-même se lit via
291
+ l'API Recherche Factures G2B, à partir du `nomFlux`.
292
+
293
+ ### Limite actuelle de l'Annuaire
294
+
295
+ En sandbox, `find_by_siret` et `find_by_siren` ne renvoient que le bloc `lignesAnnuaire` : `siret`,
296
+ `denomination`, `adresse`, `uniteLegale` et `donneesB2gComplementaires` ressortent à `nil`, y
297
+ compris pour la structure du compte appelant et même en précisant `champs`. Le mapping suit le
298
+ swagger et se remplira si l'implémentation se complète ; en attendant, l'information exploitable est
299
+ `lignes_annuaire`, qui porte les `identifiantAdressage` nécessaires à l'adressage de la facture.
300
+
301
+ ```ruby
302
+ client.annuaire.find_by_siret("71915767420316").lignes_annuaire.map(&:identifiant_adressage)
303
+ # => ["719157674_71915767420316",
304
+ # "719157674_71915767420316_FACTURES_PUBLIQUES",
305
+ # "719157674_71915767420316_SERVICE_PUBLIQUE_1_71915767420316"]
306
+ ```
307
+
308
+ ## Licence
309
+
310
+ Disponible en open source sous les termes de la [licence MIT](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << "test"
8
+ t.libs << "lib"
9
+ t.test_files = FileList["test/**/*_test.rb"].exclude("test/sandbox/**/*_test.rb")
10
+ end
11
+
12
+ namespace :test do
13
+ desc "Rejoue les cassettes VCR de la sandbox PISTE (CPRO_RECORD=1 pour ré-enregistrer)"
14
+ Rake::TestTask.new(:sandbox) do |t|
15
+ t.libs << "test"
16
+ t.libs << "lib"
17
+ t.test_files = FileList["test/sandbox/**/*_test.rb"]
18
+ end
19
+ end
20
+
21
+ require "rubocop/rake_task"
22
+
23
+ RuboCop::RakeTask.new
24
+
25
+ task default: %i[test rubocop]
data/bin/console ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "cpro/client"
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require "irb"
15
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here