cpro-client 0.2.2 → 0.2.4

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: dc05774f720962053cfcd6f9f11b4fd9521f704d9f40d4e5178a439c37b03ffd
4
- data.tar.gz: 63dc3902ac1d84c6bb49f93a0d90b4f1e5035708c69c0a635deef4475bba63d1
3
+ metadata.gz: eed2218b6135210909bb231938a2adf3d8f9b432a19468576b232ffd2f352d7a
4
+ data.tar.gz: 95739510c05be2fdc248b2f001e4b53e08b3487162dbb29f50bc26606d929e06
5
5
  SHA512:
6
- metadata.gz: e627c2faa3a057a63fc84c3fe37aca1fd3a2020175a05c0e40a0216958ff28686460d366eeae61476480a191a0f2bfcfb4eb3cf576aec3c8f0743e771f9143a7
7
- data.tar.gz: 95a17ff0f1069454907f6e415b14fdecdf75acdfd959f5d5f30dfcdf8c7532d6f56f0586d7f7fe258234a7fe28de14f6813cfbbd612201579550cbdeae9a9b3b
6
+ metadata.gz: 7208819c95be0220d40498c3d09c5df5479619c652c6103aac41903ce1cac6054f47b53a7c87638fd7b474e99581a61b6924ab1d0f17b633bb18a72d431e8a94
7
+ data.tar.gz: 4c292e8c13863bdff0fe54ab567e42880c4d25e5d928ca70fb6c1a83284198912cc14b5dd787f0733763e181a7923f08ab7768a0ee5007b5b4e52b3719c4b7f0
data/CHANGELOG.md CHANGED
@@ -1,6 +1,40 @@
1
1
  ## [Unreleased]
2
2
 
3
3
 
4
+ ## [0.2.3] - 2026-08-31
5
+
6
+ ### Ajouté
7
+ - `Cpro::Resources::Annuaire#find_identifiant_actif` résout une valeur d'adressage — SIREN, SIRET,
8
+ SIRET_SERVICE ou SIREN_SIRET_SERVICE — vers la ligne d'annuaire active la plus proche : la
9
+ valeur elle-même si sa plateforme est active, sinon son parent le plus proche, en remontant
10
+ jusqu'au SIREN. Rend `nil` lorsqu'aucun niveau n'est adressable. Un seul appel réseau, la
11
+ consultation par SIREN retournant aussi les lignes SIRET et service.
12
+ - `Cpro::Resources::Annuaire#find_lignes_adressage` rend les mêmes lignes sans filtre de
13
+ plateforme, pour distinguer « unité légale inconnue » de « connue mais sans plateforme active ».
14
+ - `Cpro::Adressage` décompose une valeur d'adressage en SIREN, SIRET et code service. Le code
15
+ service se lit comme tout ce qui suit le SIRET : il contient lui-même des underscores
16
+ (`SERVICE_PUBLIQUE_1_71915767420316`) et peut être numérique (`2026`), un découpage sur « _ »
17
+ serait donc faux. Un SIREN incohérent avec le SIRET qui le suit lève une `ArgumentError`.
18
+ - `Cpro::Entities::LigneAnnuaire#to_s` rend l'identifiant d'adressage, la valeur à porter en BT-49.
19
+
20
+ ### Corrigé
21
+ - `Cpro::Enveloppe.from` prenait un `Pathname` pour un flux, celui-ci répondant à `read` sans
22
+ porter de `path` : tout dépôt fait à partir d'un `Pathname` — la forme naturelle sous Rails, où
23
+ les chemins viennent de `Rails.root.join` — échouait sur `ArgumentError (un nom de fichier est
24
+ requis)` alors même qu'un chemin était fourni. Un `Pathname` est désormais traité comme un
25
+ chemin, donc relu en binaire plutôt que dans l'encodage par défaut du processus.
26
+ - Les sources qui sont réellement des flux passent en `binmode` avant lecture, pour qu'un IO
27
+ ouvert en mode texte ne dénature pas le PDF.
28
+
29
+
30
+ ## [0.2.2] - 2026-08-30
31
+
32
+ ### Corrigé
33
+ - `lib/cpro/middleware/json.rb` et son test étaient absents de la 0.2.1 : n'ayant pas été suivis
34
+ par git, ils étaient exclus de `spec.files`, que le gemspec construit depuis `git ls-files`. La
35
+ gem publiée échouait donc au chargement sur `require_relative`.
36
+
37
+
4
38
  ## [0.2.1] - 2026-08-28
5
39
 
6
40
  ### Modifié
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- cpro-client (0.2.2)
4
+ cpro-client (0.2.4)
5
5
  faraday (>= 0.9, < 3.0)
6
6
 
7
7
  GEM
data/README.md CHANGED
@@ -136,6 +136,34 @@ codes.first.actif?
136
136
 
137
137
  Filtrez sur `siret`, faute de quoi la réponse balaie tout l'annuaire.
138
138
 
139
+ ## Adresser une facture
140
+
141
+ BT-49, l'adresse électronique de l'acheteur, ne se transmet pas à l'API : elle voyage dans le XML
142
+ Factur-X, et c'est d'elle que le PPF déduit l'acheminement. Une facture qui la porte mal est
143
+ rejetée `REJ_ADR` — « le destinataire n'est pas trouvable » — plusieurs minutes après le dépôt.
144
+
145
+ `find_identifiant_actif` fait la résolution en un appel. Il accepte les quatre formes d'une valeur
146
+ d'adressage — SIREN, SIRET, SIRET_SERVICE, SIREN_SIRET_SERVICE — et remonte la hiérarchie
147
+ SIREN → SIRET → service jusqu'à trouver une ligne dont la plateforme est active :
148
+
149
+ ```ruby
150
+ ligne = client.annuaire.find_identifiant_actif("71915767420316")
151
+ ligne.to_s # => "719157674_71915767420316", à porter en BT-49
152
+ ligne.identifiant_routage # => "" — l'adresse de base de l'établissement
153
+
154
+ client.annuaire.find_identifiant_actif("71915767420316_SERVICE_INEXISTANT").to_s
155
+ # => "719157674_71915767420316" — service inconnu, on retombe sur son parent
156
+ ```
157
+
158
+ `nil` signifie qu'aucun niveau n'est adressable, donc qu'on ne peut pas facturer. Pour savoir
159
+ *pourquoi*, `find_lignes_adressage` rend les mêmes lignes sans filtrer : une unité légale inconnue
160
+ lève une `Cpro::NotFoundError`, tandis qu'une unité connue mais non raccordée rend ses lignes avec
161
+ `plateforme_active?` à faux.
162
+
163
+ Deux limites à connaître. La résolution ne redescend jamais vers les enfants : un SIREN nu ne
164
+ remonte rien si la structure n'a d'adresses qu'au niveau SIRET. Et une adresse active n'est pas une
165
+ promesse d'acheminement — elle garantit que le destinataire est identifiable, pas qu'il recevra.
166
+
139
167
  ## Transmission d'une facture Factur-X
140
168
 
141
169
  ```ruby
@@ -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
@@ -37,6 +37,11 @@ module Cpro
37
37
  def plateforme_en_attente?
38
38
  statut_plateforme == PLATEFORME_EN_ATTENTE
39
39
  end
40
+
41
+ # L'identifiant d'adressage est ce qu'on porte en BT-49 : c'est la valeur utile de la ligne.
42
+ def to_s
43
+ identifiant_adressage.to_s
44
+ end
40
45
  end
41
46
  end
42
47
  end
@@ -17,15 +17,28 @@ module Cpro
17
17
  attr_reader :content, :name, :code_interface
18
18
 
19
19
  def self.from(source, code_interface:, name: nil)
20
- if source.respond_to?(:read)
21
- resolved = name || (File.basename(source.path) if source.respond_to?(:path))
22
- new(content: source.read, name: resolved, code_interface: code_interface)
23
- else
24
- path = source.to_s
25
- new(content: File.binread(path), name: name || File.basename(path),
26
- code_interface: code_interface)
27
- end
20
+ return depuis_flux(source, code_interface, name) if flux?(source)
21
+
22
+ chemin = source.respond_to?(:to_path) ? source.to_path : source.to_s
23
+ new(content: File.binread(chemin), name: name || File.basename(chemin),
24
+ code_interface: code_interface)
25
+ end
26
+
27
+ # Répondre à `read` ne suffit pas à faire un flux : un Pathname en répond aussi, sans être
28
+ # ouvrable ni porter de `path`. Le distinguer par `binmode`, que tout IO possède et qu'aucun
29
+ # chemin n'a, évite de le traiter en flux anonyme — et le fait relire en binaire, là où
30
+ # Pathname#read l'aurait rendu dans l'encodage par défaut.
31
+ def self.flux?(source)
32
+ source.respond_to?(:read) && source.respond_to?(:binmode)
33
+ end
34
+ private_class_method :flux?
35
+
36
+ def self.depuis_flux(source, code_interface, name)
37
+ source.binmode
38
+ new(content: source.read, code_interface: code_interface,
39
+ name: name || (File.basename(source.path) if source.respond_to?(:path)))
28
40
  end
41
+ private_class_method :depuis_flux
29
42
 
30
43
  def initialize(content:, name:, code_interface:)
31
44
  raise ArgumentError, "le contenu du flux est vide" if content.nil? || content.empty?
@@ -77,6 +77,33 @@ module Cpro
77
77
  Entities::ResultatRecherche.new(payload, item_class: Entities::CodeRoutage, total_key: TOTAL_KEY)
78
78
  end
79
79
 
80
+ # Adresse active la plus proche d'une valeur d'adressage : la valeur elle-même si elle porte
81
+ # une plateforme active, sinon son parent le plus proche, et ainsi de suite jusqu'au SIREN.
82
+ # Rend la ligne d'annuaire, dont `to_s` donne l'identifiant à porter en BT-49.
83
+ #
84
+ # `nil` a un sens unique — aucun niveau n'est adressable, donc on ne peut pas facturer. Pour
85
+ # distinguer « SIREN inconnu » de « connu mais sans plateforme active », voir
86
+ # `#find_lignes_adressage`, qui ne filtre pas et laisse remonter le 404.
87
+ #
88
+ # Un seul appel réseau : `GET /siren/code-insee:` retourne aussi les lignes SIRET et service.
89
+ def find_identifiant_actif(valeur)
90
+ adressage = Adressage.parse(valeur)
91
+ actives = find_lignes_adressage(adressage).select(&:plateforme_active?)
92
+ adressage.paliers.each do |palier|
93
+ ligne = actives.find { |active| palier.correspond?(active) }
94
+ return ligne if ligne
95
+ end
96
+ nil
97
+ rescue NotFoundError
98
+ nil
99
+ end
100
+
101
+ # Toutes les lignes d'annuaire de l'unité légale, sans filtre de plateforme.
102
+ def find_lignes_adressage(valeur, **options)
103
+ adressage = valeur.is_a?(Adressage) ? valeur : Adressage.parse(valeur)
104
+ find_by_siren(adressage.siren, **options).lignes_annuaire
105
+ end
106
+
80
107
  def healthcheck
81
108
  get("healthcheck")
82
109
  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.2"
4
+ VERSION = "0.2.4"
5
5
  end
data/lib/cpro.rb CHANGED
@@ -6,6 +6,7 @@ require_relative "cpro/version"
6
6
  require_relative "cpro/errors"
7
7
  require_relative "cpro/configuration"
8
8
  require_relative "cpro/account"
9
+ require_relative "cpro/adressage"
9
10
  require_relative "cpro/auth/token"
10
11
  require_relative "cpro/auth/token_provider"
11
12
  require_relative "cpro/middleware/json"
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.2
4
+ version: 0.2.4
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-08-30 00:00:00.000000000 Z
11
+ date: 2026-09-04 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