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.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.rubocop.yml +42 -0
- data/CHANGELOG.md +29 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/Gemfile +13 -0
- data/Gemfile.lock +79 -0
- data/LICENSE.txt +21 -0
- data/README.md +310 -0
- data/Rakefile +25 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/cpro-client.gemspec +32 -0
- data/lib/cpro/account.rb +32 -0
- data/lib/cpro/archive.rb +44 -0
- data/lib/cpro/auth/token.rb +36 -0
- data/lib/cpro/auth/token_provider.rb +59 -0
- data/lib/cpro/cdv/message.rb +230 -0
- data/lib/cpro/cdv/statut.rb +80 -0
- data/lib/cpro/client.rb +44 -0
- data/lib/cpro/configuration.rb +89 -0
- data/lib/cpro/connection.rb +19 -0
- data/lib/cpro/entities/base.rb +71 -0
- data/lib/cpro/entities/changement_statut.rb +27 -0
- data/lib/cpro/entities/depot_flux.rb +16 -0
- data/lib/cpro/entities/etablissement.rb +96 -0
- data/lib/cpro/entities/facture.rb +85 -0
- data/lib/cpro/entities/ligne_annuaire.rb +42 -0
- data/lib/cpro/entities/lignes_annuaire.rb +45 -0
- data/lib/cpro/entities/motif_rejet.rb +24 -0
- data/lib/cpro/entities/note_statut.rb +24 -0
- data/lib/cpro/entities/resultat_recherche.rb +55 -0
- data/lib/cpro/entities/statut_flux.rb +54 -0
- data/lib/cpro/entities/unite_legale.rb +47 -0
- data/lib/cpro/enveloppe.rb +73 -0
- data/lib/cpro/errors.rb +25 -0
- data/lib/cpro/facturx.rb +53 -0
- data/lib/cpro/middleware/authentication.rb +51 -0
- data/lib/cpro/middleware/raise_error.rb +50 -0
- data/lib/cpro/resources/annuaire.rb +110 -0
- data/lib/cpro/resources/base.rb +61 -0
- data/lib/cpro/resources/factures.rb +65 -0
- data/lib/cpro/resources/flux.rb +60 -0
- data/lib/cpro/version.rb +5 -0
- data/lib/cpro-client.rb +3 -0
- data/lib/cpro.rb +56 -0
- 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
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
|
data/CODE_OF_CONDUCT.md
ADDED
|
@@ -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__)
|