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
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "zlib"
4
+ require "stringio"
5
+
6
+ module Cpro
7
+ # Inspection minimale d'un PDF/A-3 Factur-X, sans dépendance à un analyseur PDF : de quoi
8
+ # attraper le mauvais fichier avant de le déposer, pas de quoi valider la facture.
9
+ module Facturx
10
+ ENTETE_PDF = "%PDF"
11
+ RACINE_CII = "CrossIndustryInvoice"
12
+
13
+ module_function
14
+
15
+ def pdf?(contenu)
16
+ contenu.to_s.byteslice(0, ENTETE_PDF.bytesize) == ENTETE_PDF
17
+ end
18
+
19
+ # Les pièces jointes d'un PDF/A-3 sont presque toujours compressées en FlateDecode : on
20
+ # décompresse les flux jusqu'à retrouver la racine CII. Un XML stocké sans compression
21
+ # échapperait à cette recherche, d'où `xml_absent?` qui ne conclut que sur le cas certain.
22
+ def xml(contenu)
23
+ octets = contenu.to_s.b
24
+ octets.enum_for(:scan, /stream\r?\n/).each do
25
+ debut = Regexp.last_match.end(0)
26
+ fin = octets.index("endstream", debut) || next
27
+ decompresse = inflate(octets[debut...fin]) || next
28
+ return decompresse if decompresse.include?(RACINE_CII)
29
+ end
30
+ nil
31
+ end
32
+
33
+ def xml?(contenu)
34
+ !xml(contenu).nil?
35
+ end
36
+
37
+ # Montant TTC déclaré par le XML embarqué (BT-112), à comparer par l'appelant à ce qu'il
38
+ # attend. La gem ne lit pas le PDF lisible et ne peut donc rien confronter elle-même.
39
+ def montant_total(contenu)
40
+ document = xml(contenu) or return nil
41
+ valeur = document[%r{<ram:GrandTotalAmount[^>]*>([^<]+)</ram:GrandTotalAmount>}, 1]
42
+ valeur && BigDecimal(valeur.strip)
43
+ rescue ArgumentError
44
+ nil
45
+ end
46
+
47
+ def inflate(flux)
48
+ Zlib::Inflate.inflate(flux)
49
+ rescue Zlib::Error
50
+ nil
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cpro
4
+ module Middleware
5
+ # Injecte les deux niveaux d'authentification Chorus Pro : le jeton OAuth2 PISTE, commun à
6
+ # l'application, et le compte technique `cpro-account`, porté par requête via le contexte
7
+ # Faraday puisqu'il peut changer d'un appel à l'autre.
8
+ class Authentication < Faraday::Middleware
9
+ ACCOUNT_HEADER = "cpro-account"
10
+
11
+ def initialize(app, token_provider:)
12
+ super(app)
13
+ @token_provider = token_provider
14
+ end
15
+
16
+ def call(env)
17
+ body = env.body
18
+ retried = false
19
+
20
+ begin
21
+ reset(env, body)
22
+ apply(env)
23
+ @app.call(env)
24
+ rescue AuthenticationError
25
+ raise if retried
26
+
27
+ retried = true
28
+ @token_provider.invalidate!
29
+ retry
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ # Le corps de `env` porte la requête à l'aller et la réponse au retour : il faut le restaurer
36
+ # avant de rejouer l'appel, sinon la seconde tentative posterait la réponse de la première.
37
+ def reset(env, body)
38
+ env.body = body
39
+ env.response = nil
40
+ env.status = nil
41
+ env.response_headers = nil
42
+ end
43
+
44
+ def apply(env)
45
+ env.request_headers["Authorization"] = "Bearer #{@token_provider.token.value}"
46
+ account = env.request.context && env.request.context[:account]
47
+ env.request_headers[ACCOUNT_HEADER] = account.header_value if account
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cpro
4
+ module Middleware
5
+ class RaiseError < Faraday::Middleware
6
+ ERRORS = {
7
+ 400 => RequestError,
8
+ 401 => AuthenticationError,
9
+ 403 => AuthorizationError,
10
+ 404 => NotFoundError,
11
+ 406 => RequestError,
12
+ 415 => RequestError,
13
+ 422 => RequestError,
14
+ 429 => RateLimitError
15
+ }.freeze
16
+
17
+ CORRELATION_HEADER = "x-correlationid"
18
+
19
+ def on_complete(env)
20
+ return if env.status < 400
21
+
22
+ raise error_class(env.status).new(
23
+ message_for(env),
24
+ status: env.status,
25
+ body: env.body,
26
+ correlation_id: env.response_headers && env.response_headers[CORRELATION_HEADER]
27
+ )
28
+ end
29
+
30
+ private
31
+
32
+ def error_class(status)
33
+ ERRORS[status] || (status >= 500 ? ServerError : ApiError)
34
+ end
35
+
36
+ def message_for(env)
37
+ detail = detail_for(env.body) || env.reason_phrase
38
+ [env.status, detail].compact.join(" ")
39
+ end
40
+
41
+ def detail_for(body)
42
+ return body["message"] || body["error_description"] || body["error"] if body.is_a?(Hash)
43
+ return nil if body.nil?
44
+
45
+ text = body.to_s.strip
46
+ text.empty? ? nil : text[0, 500]
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cpro
4
+ module Resources
5
+ class Annuaire < Base
6
+ TOTAL_KEY = "nombre_total_resultats"
7
+
8
+ SIRET_LENGTH = 14
9
+ SIREN_LENGTH = 9
10
+
11
+ # Chaque champ filtrable n'admet qu'un seul opérateur dans le swagger : l'appelant fournit
12
+ # `siret: "702042755"` et l'opérateur attendu est déduit d'ici.
13
+ OPERATEURS_SIRET = {
14
+ "siret" => "contient",
15
+ "siren" => "contient",
16
+ "denomination" => "contient",
17
+ "localite" => "contient",
18
+ "codePostal" => "contient",
19
+ "lignesAdresse" => "contient",
20
+ "subDivisionPays" => "contient",
21
+ "typeEtablissement" => "strict",
22
+ "etatAdministratif" => "strict"
23
+ }.freeze
24
+
25
+ OPERATEURS_SIREN = {
26
+ "siren" => "contient",
27
+ "raisonSociale" => "contient",
28
+ "typeEntite" => "strict",
29
+ "etatAdministratif" => "strict"
30
+ }.freeze
31
+
32
+ def find_by_siret(siret, **options)
33
+ path = "siret/code-insee:#{normalize(siret, SIRET_LENGTH, "SIRET")}"
34
+ Entities::Etablissement.new(get(path, query(options)))
35
+ end
36
+
37
+ def find_by_siren(siren, **options)
38
+ path = "siren/code-insee:#{normalize(siren, SIREN_LENGTH, "SIREN")}"
39
+ Entities::UniteLegale.new(get(path, query(options)))
40
+ end
41
+
42
+ # `filtres` est un mot-clé et non un positionnel : avec `search_siret(filtres, **options)`,
43
+ # un appel de la forme `search_siret(siren: "702042755")` verrait tous ses mots-clés partir
44
+ # dans `options` sous Ruby 3 et lèverait une ArgumentError.
45
+ def search_siret(filtres: {}, **options)
46
+ payload = post("siret/recherche", search_body(OPERATEURS_SIRET, filtres, options))
47
+ Entities::ResultatRecherche.new(payload, item_class: Entities::Etablissement, total_key: TOTAL_KEY)
48
+ end
49
+
50
+ def search_siren(filtres: {}, **options)
51
+ payload = post("siren/recherche", search_body(OPERATEURS_SIREN, filtres, options))
52
+ Entities::ResultatRecherche.new(payload, item_class: Entities::UniteLegale, total_key: TOTAL_KEY)
53
+ end
54
+
55
+ def healthcheck
56
+ get("healthcheck")
57
+ end
58
+
59
+ private
60
+
61
+ def normalize(value, length, label)
62
+ digits = value.to_s.gsub(/\s+/, "")
63
+ return digits if digits.match?(/\A\d{#{length}}\z/)
64
+
65
+ raise ArgumentError, "#{label} invalide : #{value.inspect} (#{length} chiffres attendus)"
66
+ end
67
+
68
+ # `champs` et `inclure` sont déclarés `explode: false` : ils voyagent en liste séparée par
69
+ # des virgules, et non répétés.
70
+ def query(options)
71
+ {
72
+ champs: join(options[:champs]),
73
+ inclure: join(options[:inclure]),
74
+ limite: options[:limite],
75
+ ignorer: options[:ignorer]
76
+ }.compact
77
+ end
78
+
79
+ def join(value)
80
+ return nil if value.nil?
81
+
82
+ Array(value).join(",")
83
+ end
84
+
85
+ def search_body(operateurs, filtres, options)
86
+ compact_body(
87
+ "filtres" => build_filtres(operateurs, filtres),
88
+ "champs" => Array(options[:champs]).map(&:to_s),
89
+ "inclure" => Array(options[:inclure]).map(&:to_s),
90
+ "tris" => build_tris(options[:tris]),
91
+ "limite" => options[:limite],
92
+ "ignorer" => options[:ignorer]
93
+ )
94
+ end
95
+
96
+ def build_filtres(operateurs, filtres)
97
+ (filtres || {}).each_with_object({}) do |(champ, valeur), acc|
98
+ key = champ.to_s
99
+ operateur = operateurs[key]
100
+ unless operateur
101
+ raise ArgumentError,
102
+ "filtre inconnu : #{champ.inspect} (attendus : #{operateurs.keys.join(", ")})"
103
+ end
104
+
105
+ acc[key] = { "op" => operateur, "valeur" => valeur.to_s }
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cpro
4
+ module Resources
5
+ class Base
6
+ def initialize(client, connection)
7
+ @client = client
8
+ @connection = connection
9
+ end
10
+
11
+ private
12
+
13
+ attr_reader :client, :connection
14
+
15
+ def get(path, params = {})
16
+ request(:get, path, params: params)
17
+ end
18
+
19
+ def post(path, body)
20
+ request(:post, path, body: body)
21
+ end
22
+
23
+ # Le swagger Annuaire ne déclare pas cpro-account, mais l'API le rejette en 400 sans lui,
24
+ # healthcheck compris : toutes les ressources G2B l'exigent (vérifié en sandbox).
25
+ def request(method, path, params: nil, body: nil)
26
+ response = connection.public_send(method) do |request|
27
+ request.url(path, params || {})
28
+ request.body = body unless body.nil?
29
+ request.options.context = { account: required_account }
30
+ end
31
+ response.body
32
+ end
33
+
34
+ def required_account
35
+ account = client.account
36
+ return account if account
37
+
38
+ raise ConfigurationError,
39
+ "un compte technique est requis pour #{self.class.name} : " \
40
+ "passez-le via Cpro::Client#with_account ou Cpro::Configuration#account"
41
+ end
42
+
43
+ def normalize_uid(uid, label)
44
+ value = uid.respond_to?(:uid) ? uid.uid : uid
45
+ raise ArgumentError, "#{label} manquant" if value.nil? || value.to_s.empty?
46
+
47
+ value.to_s
48
+ end
49
+
50
+ def build_tris(tris)
51
+ return nil if tris.nil? || tris.empty?
52
+
53
+ tris.map { |champ, ordre| { "champ" => champ.to_s, "ordre" => ordre.to_s } }
54
+ end
55
+
56
+ def compact_body(body)
57
+ body.reject { |_, value| value.nil? || (value.respond_to?(:empty?) && value.empty?) }
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cpro
4
+ module Resources
5
+ class Factures < Base
6
+ # Contrairement à l'Annuaire, les filtres sont plats : `{"nomFlux" => "CPP004…"}` et non
7
+ # `{"op" => …, "valeur" => …}`. Les valeurs partent telles quelles, `codeTypeFacture` étant
8
+ # un entier.
9
+ FILTRES = %w[
10
+ numeroFacture dateEmissionFactureDu dateEmissionFactureAu codeTypeFacture idObjetFacture
11
+ siretVendeur siretAgentVendeur siretAcheteur sirenAcheteur siretTiersFacturant
12
+ dateStatutFactureDu dateStatutFactureAu nomFlux
13
+ ].freeze
14
+
15
+ TOTAL_KEY = "nombreTotalResultats"
16
+
17
+ def search(filtres:, **options)
18
+ payload = post("facture/recherche", search_body(filtres, options))
19
+ Entities::ResultatRecherche.new(payload, item_class: Entities::Facture, total_key: TOTAL_KEY)
20
+ end
21
+
22
+ # Cinématique documentée après un dépôt : le nomFlux rendu par `Flux#status` mène aux
23
+ # factures que le flux contenait.
24
+ def search_by_nom_flux(nom_flux, **options)
25
+ search(filtres: { nomFlux: nom_flux.respond_to?(:nom_flux) ? nom_flux.nom_flux : nom_flux }, **options)
26
+ end
27
+
28
+ def find(uid)
29
+ payload = get("facture/#{normalize_uid(uid, "identifiant de facture")}")
30
+ payload = {} unless payload.is_a?(Hash)
31
+
32
+ Entities::Facture.new(payload["facture"], historique: payload["historique"])
33
+ end
34
+
35
+ def healthcheck
36
+ get("healthcheck")
37
+ end
38
+
39
+ private
40
+
41
+ def search_body(filtres, options)
42
+ compact_body(
43
+ "filtres" => build_filtres(filtres),
44
+ "champs" => Array(options[:champs]).map(&:to_s),
45
+ "tris" => build_tris(options[:tris]),
46
+ "limite" => options[:limite],
47
+ "ignorer" => options[:ignorer]
48
+ )
49
+ end
50
+
51
+ def build_filtres(filtres)
52
+ raise ArgumentError, "au moins un filtre est requis" if filtres.nil? || filtres.empty?
53
+
54
+ filtres.each_with_object({}) do |(champ, valeur), acc|
55
+ key = champ.to_s
56
+ unless FILTRES.include?(key)
57
+ raise ArgumentError, "filtre inconnu : #{champ.inspect} (attendus : #{FILTRES.join(", ")})"
58
+ end
59
+
60
+ acc[key] = valeur
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cpro
4
+ module Resources
5
+ class Flux < Base
6
+ def deposit_facturx(source, name: nil)
7
+ depot(source, :facturx, name: name) { |contenu| verifier_facturx(contenu) }
8
+ end
9
+
10
+ def deposit_ubl(source, name: nil)
11
+ depot(source, :ubl, name: name)
12
+ end
13
+
14
+ # Émettre un statut de cycle de vie : le fichier attendu est un CDAR D22B conforme à
15
+ # l'Annexe A de la norme AFNOR XP Z12-012. La gem le transporte, elle ne le fabrique pas.
16
+ def deposit_cdv(source, name: nil)
17
+ depot(source, :cdv, name: name)
18
+ end
19
+
20
+ def deposit_ereporting(source, name: nil)
21
+ depot(source, :ereporting, name: name)
22
+ end
23
+
24
+ def emettre_statut(facture, statut, **options)
25
+ message = Cdv::Message.new(facture: facture, statut: statut, **options)
26
+ depot(StringIO.new(message.to_xml), :cdv, name: message.nom_fichier)
27
+ end
28
+
29
+ def status(uid)
30
+ status!(uid)
31
+ rescue NotFoundError
32
+ nil
33
+ end
34
+
35
+ def status!(uid)
36
+ Entities::StatutFlux.new(get("flux/#{normalize_uid(uid, "identifiant de flux")}"))
37
+ end
38
+
39
+ def healthcheck
40
+ get("healthcheck")
41
+ end
42
+
43
+ private
44
+
45
+ def depot(source, code_interface, name:)
46
+ enveloppe = Enveloppe.from(source, code_interface: code_interface, name: name)
47
+ yield enveloppe.content if block_given?
48
+ Entities::DepotFlux.new(post("flux", enveloppe.payload))
49
+ end
50
+
51
+ def verifier_facturx(contenu)
52
+ raise ArgumentError, "le fichier déposé n'est pas un PDF" unless Facturx.pdf?(contenu)
53
+ return if Facturx.xml?(contenu)
54
+
55
+ raise ArgumentError, "aucun XML Factur-X n'a été trouvé dans le PDF : vérifiez qu'il " \
56
+ "s'agit bien d'un PDF/A-3 avec sa pièce jointe factur-x.xml"
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cpro
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "cpro"
data/lib/cpro.rb ADDED
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "faraday"
4
+
5
+ require_relative "cpro/version"
6
+ require_relative "cpro/errors"
7
+ require_relative "cpro/configuration"
8
+ require_relative "cpro/account"
9
+ require_relative "cpro/auth/token"
10
+ require_relative "cpro/auth/token_provider"
11
+ require_relative "cpro/middleware/authentication"
12
+ require_relative "cpro/middleware/raise_error"
13
+ require_relative "cpro/connection"
14
+ require_relative "cpro/entities/base"
15
+ require_relative "cpro/entities/ligne_annuaire"
16
+ require_relative "cpro/entities/lignes_annuaire"
17
+ require_relative "cpro/entities/unite_legale"
18
+ require_relative "cpro/entities/etablissement"
19
+ require_relative "cpro/entities/note_statut"
20
+ require_relative "cpro/entities/motif_rejet"
21
+ require_relative "cpro/entities/changement_statut"
22
+ require_relative "cpro/entities/facture"
23
+ require_relative "cpro/entities/resultat_recherche"
24
+ require_relative "cpro/entities/depot_flux"
25
+ require_relative "cpro/entities/statut_flux"
26
+ require_relative "cpro/cdv/statut"
27
+ require_relative "cpro/cdv/message"
28
+ require_relative "cpro/facturx"
29
+ require_relative "cpro/archive"
30
+ require_relative "cpro/enveloppe"
31
+ require_relative "cpro/resources/base"
32
+ require_relative "cpro/resources/annuaire"
33
+ require_relative "cpro/resources/flux"
34
+ require_relative "cpro/resources/factures"
35
+ require_relative "cpro/client"
36
+
37
+ module Cpro
38
+ class << self
39
+ def configuration
40
+ @configuration ||= Configuration.new
41
+ end
42
+
43
+ def configure
44
+ yield configuration
45
+ configuration
46
+ end
47
+
48
+ def client(**options)
49
+ Client.new(configuration, **options)
50
+ end
51
+
52
+ def reset!
53
+ @configuration = nil
54
+ end
55
+ end
56
+ end
metadata ADDED
@@ -0,0 +1,106 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cpro-client
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Hôtentic
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2026-08-27 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: faraday
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.0'
27
+ description: 'Client Ruby des API G2B de Chorus Pro (PPF) : authentification OAuth2
28
+ PISTE, consultation de l''annuaire par SIRET ou SIREN, et dépôt de factures Factur-X.'
29
+ email:
30
+ - contact@hotentic.com
31
+ executables: []
32
+ extensions: []
33
+ extra_rdoc_files: []
34
+ files:
35
+ - ".gitignore"
36
+ - ".rubocop.yml"
37
+ - CHANGELOG.md
38
+ - CODE_OF_CONDUCT.md
39
+ - Gemfile
40
+ - Gemfile.lock
41
+ - LICENSE.txt
42
+ - README.md
43
+ - Rakefile
44
+ - bin/console
45
+ - bin/setup
46
+ - cpro-client.gemspec
47
+ - lib/cpro-client.rb
48
+ - lib/cpro.rb
49
+ - lib/cpro/account.rb
50
+ - lib/cpro/archive.rb
51
+ - lib/cpro/auth/token.rb
52
+ - lib/cpro/auth/token_provider.rb
53
+ - lib/cpro/cdv/message.rb
54
+ - lib/cpro/cdv/statut.rb
55
+ - lib/cpro/client.rb
56
+ - lib/cpro/configuration.rb
57
+ - lib/cpro/connection.rb
58
+ - lib/cpro/entities/base.rb
59
+ - lib/cpro/entities/changement_statut.rb
60
+ - lib/cpro/entities/depot_flux.rb
61
+ - lib/cpro/entities/etablissement.rb
62
+ - lib/cpro/entities/facture.rb
63
+ - lib/cpro/entities/ligne_annuaire.rb
64
+ - lib/cpro/entities/lignes_annuaire.rb
65
+ - lib/cpro/entities/motif_rejet.rb
66
+ - lib/cpro/entities/note_statut.rb
67
+ - lib/cpro/entities/resultat_recherche.rb
68
+ - lib/cpro/entities/statut_flux.rb
69
+ - lib/cpro/entities/unite_legale.rb
70
+ - lib/cpro/enveloppe.rb
71
+ - lib/cpro/errors.rb
72
+ - lib/cpro/facturx.rb
73
+ - lib/cpro/middleware/authentication.rb
74
+ - lib/cpro/middleware/raise_error.rb
75
+ - lib/cpro/resources/annuaire.rb
76
+ - lib/cpro/resources/base.rb
77
+ - lib/cpro/resources/factures.rb
78
+ - lib/cpro/resources/flux.rb
79
+ - lib/cpro/version.rb
80
+ homepage: https://hotentic.com
81
+ licenses:
82
+ - MIT
83
+ metadata:
84
+ homepage_uri: https://hotentic.com
85
+ source_code_uri: https://github.com/hotentic/cpro-client
86
+ changelog_uri: https://github.com/hotentic/cpro-client/releases
87
+ post_install_message:
88
+ rdoc_options: []
89
+ require_paths:
90
+ - lib
91
+ required_ruby_version: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: 2.7.0
96
+ required_rubygems_version: !ruby/object:Gem::Requirement
97
+ requirements:
98
+ - - ">="
99
+ - !ruby/object:Gem::Version
100
+ version: '0'
101
+ requirements: []
102
+ rubygems_version: 3.1.6
103
+ signing_key:
104
+ specification_version: 4
105
+ summary: A ruby client for the Chorus Pro API
106
+ test_files: []