cpro-client 0.2.3 → 0.2.5
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 +24 -0
- data/Gemfile.lock +1 -1
- data/README.md +42 -0
- data/lib/cpro/adressage.rb +70 -0
- data/lib/cpro/client.rb +6 -0
- data/lib/cpro/entities/facture.rb +35 -0
- 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: fcf97fbee94114c1241fb76a3b3ec620dd7d8c2e40435bebfe146ce7b65a3e92
|
|
4
|
+
data.tar.gz: 0b3e4f80fe36110c60a46d7adc951cb0bd662093a30e39a0f19fb9e4c252096d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4f010893e86d5f3dec2742aca27ea56606f6facb294c23cc613f91073d4ebf3b3f56dc1fdea650121b76a706680a65bd4921c875230a0e00beb428d4306f7876
|
|
7
|
+
data.tar.gz: 65724ca9625aa0328e6d3b14f10005f087614e589587f73e706b17d515b1091f3633f960bf9036a8031e30abdab360814856d076de551eaa0f5a860759b3a0a9
|
data/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,30 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
3
|
|
|
4
|
+
## [0.2.5] - 2026-09-05
|
|
5
|
+
|
|
6
|
+
### Ajouté
|
|
7
|
+
- `Cpro::Client#suivi` enchaîne les appels de suivi d'un dépôt — statut du flux, recherche de la
|
|
8
|
+
facture, consultation avec son historique — et rend un `Cpro::Suivi` qui nomme l'étape atteinte :
|
|
9
|
+
`:flux_en_attente` (404 du PPF, cas nominal juste après un dépôt), `:flux_irrecevable`,
|
|
10
|
+
`:facture_introuvable` (204, ambigu par nature) ou `:facture_connue`. Il court-circuite dès
|
|
11
|
+
qu'un palier interdit d'aller plus loin : un appel tant que le flux n'est pas traité, trois au
|
|
12
|
+
plus. Il n'attend jamais — la cadence d'interrogation appartient à l'appelant — et laisse
|
|
13
|
+
`statut_flux` et `facture` accessibles pour ce qu'il n'interprète pas.
|
|
14
|
+
- `Cpro::Entities::Facture#dernier_incident`, relayé par `Cpro::Suivi#dernier_incident`, isole le
|
|
15
|
+
dernier changement de statut porteur de motifs — l'incident en cours — là où `motifs_rejet`
|
|
16
|
+
cumule tout l'historique. Il rend le changement et non le motif : un même changement en porte
|
|
17
|
+
plusieurs, une facture « Déposée » pouvant cumuler `NON_TRANSMISE` et `REJ_UNI`.
|
|
18
|
+
- `Cpro::Suivi#motifs` rassemble les motifs de rejet, qu'ils viennent du flux ou de l'historique de
|
|
19
|
+
la facture : l'appelant n'a plus à savoir où le dépôt s'est arrêté pour aller les chercher.
|
|
20
|
+
- `Cpro::Entities::Facture#en_echec?` — vrai dès qu'une facture demande une intervention, que son
|
|
21
|
+
statut l'annonce ou non. Chorus Pro accroche ses motifs de rejet au statut porteur, qui peut
|
|
22
|
+
être anodin : une facture « Déposée » assortie de `NON_TRANSMISE` ne parviendra pas à son
|
|
23
|
+
destinataire alors que `rejetee?` répond faux. Le prédicat couvre les statuts `REJETEE` et
|
|
24
|
+
`ERREUR_ROUTAGE` ainsi que tout motif présent dans l'historique — lequel n'est renseigné que
|
|
25
|
+
par `Factures#find`.
|
|
26
|
+
|
|
27
|
+
|
|
4
28
|
## [0.2.3] - 2026-08-31
|
|
5
29
|
|
|
6
30
|
### Ajouté
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
|
@@ -254,6 +254,48 @@ aucune facture ne correspond que lorsque le compte technique n'est rattaché ni
|
|
|
254
254
|
callback, et les statuts des factures G2B ne sont pas consultables sur le portail Chorus Pro : le
|
|
255
255
|
suivi passe nécessairement par une interrogation périodique de cette API.
|
|
256
256
|
|
|
257
|
+
## Suivre un dépôt en un appel
|
|
258
|
+
|
|
259
|
+
Les quatre étapes du suivi — statut du flux, recherche par `nomFlux`, consultation, lecture des
|
|
260
|
+
motifs — se réduisent à une méthode, qui nomme l'étape atteinte plutôt que de lever ou de rendre
|
|
261
|
+
`nil` :
|
|
262
|
+
|
|
263
|
+
```ruby
|
|
264
|
+
suivi = client.suivi(depot)
|
|
265
|
+
|
|
266
|
+
suivi.etape # :flux_en_attente, :flux_irrecevable, :facture_introuvable, :facture_connue
|
|
267
|
+
suivi.en_echec? # au niveau de l'enveloppe comme à celui de la facture
|
|
268
|
+
suivi.motifs # les motifs, d'où qu'ils viennent
|
|
269
|
+
suivi.facture # la facture et son historique, à l'étape :facture_connue
|
|
270
|
+
suivi.statut_flux # le statut brut du flux, dès qu'il est connu
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
`motifs` cumule tout l'historique — un incident réglé y côtoie l'incident courant. Pour n'avoir que
|
|
274
|
+
le plus récent, avec le statut qui le portait et sa date :
|
|
275
|
+
|
|
276
|
+
```ruby
|
|
277
|
+
incident = suivi.dernier_incident # un ChangementStatut, ou nil
|
|
278
|
+
|
|
279
|
+
incident.statut # "DEPOSEE"
|
|
280
|
+
incident.date # 2026-09-04 19:18:31 UTC
|
|
281
|
+
incident.motifs_rejet.map(&:code) # ["NON_TRANSMISE", "REJ_UNI"]
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
Il rend le changement et non le motif : un même changement en porte plusieurs, et n'en exposer
|
|
285
|
+
qu'un les masquerait. Il vaut `nil` à l'étape `:flux_irrecevable`, où il n'y a pas encore de
|
|
286
|
+
facture — `motifs` reste alors l'accès uniforme.
|
|
287
|
+
|
|
288
|
+
| Étape | Ce qui s'est passé | Appels | Suite |
|
|
289
|
+
| --- | --- | :-: | --- |
|
|
290
|
+
| `:flux_en_attente` | 404 sur le flux — cas nominal | 1 | réinterroger |
|
|
291
|
+
| `:flux_irrecevable` | enveloppe refusée, motifs sur le flux | 1 | terminal |
|
|
292
|
+
| `:facture_introuvable` | flux recevable, recherche vide | 2 | réinterroger, puis vérifier le rattachement du compte |
|
|
293
|
+
| `:facture_connue` | facture et historique disponibles | 3 | lire `en_echec?` et les motifs |
|
|
294
|
+
|
|
295
|
+
La méthode **n'attend pas** : c'est une photographie, la cadence d'interrogation reste à l'appelant.
|
|
296
|
+
Seuls le 404 du flux et la recherche vide deviennent des étapes ; un 401, un 403 ou un 5xx
|
|
297
|
+
continuent de lever.
|
|
298
|
+
|
|
257
299
|
## Servir plusieurs entités
|
|
258
300
|
|
|
259
301
|
L'AIFE distingue trois types de raccordement API. Celui qui s'applique détermine le modèle à
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cpro
|
|
4
|
+
# Situe une valeur d'adressage dans la hiérarchie de l'annuaire : SIREN → SIRET → service.
|
|
5
|
+
# Quatre formes sont acceptées — SIREN, SIRET, SIRET_SERVICE et SIREN_SIRET_SERVICE.
|
|
6
|
+
#
|
|
7
|
+
# Le service ne se lit jamais par découpage sur « _ » : il en contient lui-même
|
|
8
|
+
# (SERVICE_PUBLIQUE_1_71915767420316) et peut être purement numérique ("2026"). Il se lit comme
|
|
9
|
+
# tout ce qui suit le SIRET.
|
|
10
|
+
class Adressage
|
|
11
|
+
PREFIXE_SIREN = /\A(\d{9})_(?=\d{14})/.freeze
|
|
12
|
+
SIRET = /\A(\d{14})/.freeze
|
|
13
|
+
SIREN_SEUL = /\A(\d{9})\z/.freeze
|
|
14
|
+
|
|
15
|
+
attr_reader :siren, :siret, :service
|
|
16
|
+
|
|
17
|
+
def self.parse(valeur)
|
|
18
|
+
texte = valeur.to_s.strip
|
|
19
|
+
raise ArgumentError, "valeur d'adressage vide" if texte.empty?
|
|
20
|
+
|
|
21
|
+
prefixe = texte[PREFIXE_SIREN, 1]
|
|
22
|
+
reste = prefixe ? texte[10..] : texte
|
|
23
|
+
depuis_siret(prefixe, reste) || depuis_siren(reste) ||
|
|
24
|
+
raise(ArgumentError, "valeur d'adressage illisible : #{valeur.inspect} " \
|
|
25
|
+
"(attendus : SIREN, SIRET, SIRET_SERVICE ou SIREN_SIRET_SERVICE)")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.depuis_siret(prefixe, reste)
|
|
29
|
+
siret = reste[SIRET, 1]
|
|
30
|
+
return nil if siret.nil?
|
|
31
|
+
|
|
32
|
+
raise ArgumentError, "SIREN #{prefixe} incohérent avec le SIRET #{siret}" if prefixe && prefixe != siret[0, 9]
|
|
33
|
+
|
|
34
|
+
new(siren: siret[0, 9], siret: siret, service: reste[14..].to_s.sub(/\A_/, ""))
|
|
35
|
+
end
|
|
36
|
+
private_class_method :depuis_siret
|
|
37
|
+
|
|
38
|
+
def self.depuis_siren(reste)
|
|
39
|
+
siren = reste[SIREN_SEUL, 1]
|
|
40
|
+
siren && new(siren: siren, siret: nil, service: nil)
|
|
41
|
+
end
|
|
42
|
+
private_class_method :depuis_siren
|
|
43
|
+
|
|
44
|
+
def initialize(siren:, siret: nil, service: nil)
|
|
45
|
+
@siren = siren
|
|
46
|
+
@siret = siret
|
|
47
|
+
@service = service.nil? || service.empty? ? nil : service
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Du plus précis au plus général : la valeur elle-même, puis ses parents successifs. C'est
|
|
51
|
+
# l'ordre dans lequel une adresse active est cherchée.
|
|
52
|
+
def paliers
|
|
53
|
+
[
|
|
54
|
+
(self.class.new(siren: siren, siret: siret, service: service) if service),
|
|
55
|
+
(self.class.new(siren: siren, siret: siret) if siret),
|
|
56
|
+
self.class.new(siren: siren)
|
|
57
|
+
].compact
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Une ligne d'annuaire correspond à ce palier si elle décrit le même niveau de précision.
|
|
61
|
+
def correspond?(ligne)
|
|
62
|
+
ligne.siret == siret && ligne.identifiant_routage.to_s.empty? == service.nil? &&
|
|
63
|
+
(service.nil? || ligne.identifiant_routage == service)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def to_s
|
|
67
|
+
[siret ? "#{siren}_#{siret}" : siren, service].compact.join("_")
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
data/lib/cpro/client.rb
CHANGED
|
@@ -19,6 +19,12 @@ module Cpro
|
|
|
19
19
|
self.class.new(configuration, account: account, token_provider: token_provider, connections: connections)
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
+
# Enchaîne les appels de suivi d'un dépôt et rend une synthèse. Traverse deux ressources,
|
|
23
|
+
# d'où sa place ici plutôt que sur l'une d'elles.
|
|
24
|
+
def suivi(depot)
|
|
25
|
+
Suivi.pour(self, depot)
|
|
26
|
+
end
|
|
27
|
+
|
|
22
28
|
def annuaire
|
|
23
29
|
@annuaire ||= Resources::Annuaire.new(self, connection_for(ANNUAIRE_PATH))
|
|
24
30
|
end
|
|
@@ -10,6 +10,11 @@ module Cpro
|
|
|
10
10
|
CHANGEMENT_DE_COMPTE_A_PAYER NON_AFFACTUREE AFFACTUREE AFFACTUREE_CONFIDENTIEL
|
|
11
11
|
].freeze
|
|
12
12
|
|
|
13
|
+
EPOQUE = Time.at(0).utc
|
|
14
|
+
|
|
15
|
+
# Statuts qui constatent par eux-mêmes qu'une facture n'arrivera pas.
|
|
16
|
+
STATUTS_EN_ECHEC = %w[REJETEE ERREUR_ROUTAGE].freeze
|
|
17
|
+
|
|
13
18
|
CHAMPS = {
|
|
14
19
|
uid: "uidFacture",
|
|
15
20
|
numero: "numeroFacture",
|
|
@@ -77,6 +82,36 @@ module Cpro
|
|
|
77
82
|
historique.flat_map(&:motifs_rejet)
|
|
78
83
|
end
|
|
79
84
|
|
|
85
|
+
# Le dernier changement de statut porteur de motifs — celui qui décrit l'incident en cours,
|
|
86
|
+
# là où `motifs_rejet` cumule tout l'historique, motifs devenus caducs compris.
|
|
87
|
+
#
|
|
88
|
+
# Rend le changement et non le motif : un même changement en porte plusieurs — une facture
|
|
89
|
+
# « Déposée » peut cumuler NON_TRANSMISE et REJ_UNI — et n'en exposer qu'un les masquerait.
|
|
90
|
+
# Le rang départage deux changements de même date, le plus récent l'emportant.
|
|
91
|
+
def dernier_incident
|
|
92
|
+
incidents = historique.select(&:rejet?)
|
|
93
|
+
incidents.each_with_index.max_by { |changement, rang| [changement.date || EPOQUE, rang] }&.first
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Vrai dès qu'une facture demande une intervention, que son statut l'annonce ou non : un
|
|
97
|
+
# motif de rejet peut être porté par un statut anodin. Une facture « Déposée » accompagnée
|
|
98
|
+
# de NON_TRANSMISE en est le cas type — `rejetee?` répond faux, et pourtant le destinataire
|
|
99
|
+
# ne la recevra pas par la plateforme.
|
|
100
|
+
#
|
|
101
|
+
# Attention au mot « échec » : NON_TRANSMISE n'invalide pas la facture. Le dossier de
|
|
102
|
+
# spécifications externes est explicite — ce statut « permet à l'entité publique de
|
|
103
|
+
# justifier qu'elle a bien émis une facture électronique et qu'elle a donc respecté ses
|
|
104
|
+
# obligations réglementaires ». Il n'y a donc rien à redéposer ; il y a à livrer autrement.
|
|
105
|
+
#
|
|
106
|
+
# Le refus de l'acheteur (REFUSEE) n'en fait pas partie : c'est une décision métier, une
|
|
107
|
+
# étape normale du cycle de vie, pas un incident. Elle se lit par `refusee?`.
|
|
108
|
+
#
|
|
109
|
+
# L'historique n'étant renseigné que par `Factures#find`, une facture issue d'une recherche
|
|
110
|
+
# ne peut être jugée que sur son statut.
|
|
111
|
+
def en_echec?
|
|
112
|
+
STATUTS_EN_ECHEC.include?(statut) || historique.any?(&:rejet?)
|
|
113
|
+
end
|
|
114
|
+
|
|
80
115
|
def to_s
|
|
81
116
|
[numero, statut].compact.join(" ")
|
|
82
117
|
end
|
data/lib/cpro/version.rb
CHANGED
data/lib/cpro.rb
CHANGED
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.
|
|
4
|
+
version: 0.2.5
|
|
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-
|
|
11
|
+
date: 2026-09-05 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: faraday
|
|
@@ -53,6 +53,7 @@ files:
|
|
|
53
53
|
- lib/cpro-client.rb
|
|
54
54
|
- lib/cpro.rb
|
|
55
55
|
- lib/cpro/account.rb
|
|
56
|
+
- lib/cpro/adressage.rb
|
|
56
57
|
- lib/cpro/archive.rb
|
|
57
58
|
- lib/cpro/auth/token.rb
|
|
58
59
|
- lib/cpro/auth/token_provider.rb
|